using System;
using System.Collections.Generic;
using System.Linq;
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, 1), 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("substr", new ClrFunctionInstance(Engine, Substr, 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 JsValue ToStringString(JsValue thisObj, JsValue[] arguments)
{
var s = TypeConverter.ToObject(Engine, thisObj) as StringInstance;
if (s == null)
{
throw new JavaScriptException(Engine.TypeError);
}
return s.PrimitiveValue;
}
// http://msdn.microsoft.com/en-us/library/system.char.iswhitespace(v=vs.110).aspx
// http://en.wikipedia.org/wiki/Byte_order_mark
const char BOM_CHAR = '\uFEFF';
private static bool IsWhiteSpaceEx(char c)
{
return char.IsWhiteSpace(c) || c == BOM_CHAR;
}
private static string TrimEndEx(string s)
{
if (s.Length == 0)
return string.Empty;
var i = s.Length - 1;
while (i >= 0)
{
if (IsWhiteSpaceEx(s[i]))
i--;
else
break;
}
if (i >= 0)
return s.Substring(0, i + 1);
else
return string.Empty;
}
private static string TrimStartEx(string s)
{
if (s.Length == 0)
return string.Empty;
var i = 0;
while (i < s.Length)
{
if (IsWhiteSpaceEx(s[i]))
i++;
else
break;
}
if (i >= s.Length)
return string.Empty;
else
return s.Substring(i);
}
private static string TrimEx(string s)
{
return TrimEndEx(TrimStartEx(s));
}
private JsValue Trim(JsValue thisObj, JsValue[] arguments)
{
TypeConverter.CheckObjectCoercible(Engine, thisObj);
var s = TypeConverter.ToString(thisObj);
return TrimEx(s);
}
private static JsValue ToLocaleUpperCase(JsValue thisObj, JsValue[] arguments)
{
var s = TypeConverter.ToString(thisObj);
return s.ToUpper();
}
private static JsValue ToUpperCase(JsValue thisObj, JsValue[] arguments)
{
var s = TypeConverter.ToString(thisObj);
return s.ToUpperInvariant();
}
private static JsValue ToLocaleLowerCase(JsValue thisObj, JsValue[] arguments)
{
var s = TypeConverter.ToString(thisObj);
return s.ToLower();
}
private static JsValue ToLowerCase(JsValue thisObj, JsValue[] arguments)
{
var s = TypeConverter.ToString(thisObj);
return s.ToLowerInvariant();
}
private static int ToIntegerSupportInfinity(JsValue numberVal)
{
var doubleVal = TypeConverter.ToInteger(numberVal);
var intVal = (int) doubleVal;
if (double.IsPositiveInfinity(doubleVal))
intVal = int.MaxValue;
else if (double.IsNegativeInfinity(doubleVal))
intVal = int.MinValue;
else
intVal = (int) doubleVal;
return intVal;
}
private JsValue Substring(JsValue thisObj, JsValue[] 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 = ToIntegerSupportInfinity(start);
var intEnd = arguments.At(1) == Undefined.Instance ? len : (int)ToIntegerSupportInfinity(end);
var finalStart = System.Math.Min(len, System.Math.Max(intStart, 0));
var finalEnd = System.Math.Min(len, System.Math.Max(intEnd, 0));
// Swap value if finalStart < finalEnd
var from = System.Math.Min(finalStart, finalEnd);
var to = System.Math.Max(finalStart, finalEnd);
return s.Substring(from, to - from);
}
private JsValue Substr(JsValue thisObj, JsValue[] arguments)
{
var s = TypeConverter.ToString(thisObj);
var start = TypeConverter.ToInteger(arguments.At(0));
var length = arguments.At(1) == JsValue.Undefined
? double.PositiveInfinity
: TypeConverter.ToInteger(arguments.At(1));
start = start >= 0 ? start : System.Math.Max(s.Length + start, 0);
length = System.Math.Min(System.Math.Max(length, 0), s.Length - start);
if (length <= 0)
{
return "";
}
return s.Substring(TypeConverter.ToInt32(start), TypeConverter.ToInt32(length));
}
private JsValue Split(JsValue thisObj, JsValue[] arguments)
{
TypeConverter.CheckObjectCoercible(Engine, thisObj);
var s = TypeConverter.ToString(thisObj);
var separator = arguments.At(0);
// Coerce into a number, true will become 1
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 == Null.Instance)
{
separator = Null.Text;
}
else if (separator == Undefined.Instance)
{
return (ArrayInstance)Engine.Array.Construct(Arguments.From(s));
}
else
{
if (!separator.IsRegExp())
{
separator = TypeConverter.ToString(separator); // Coerce into a string, for an object call toString()
}
}
var rx = TypeConverter.ToObject(Engine, separator) as RegExpInstance;
const string regExpForMatchingAllCharactere = "(?:)";
if (rx != null &&
rx.Source != regExpForMatchingAllCharactere // We need pattern to be defined -> for s.split(new RegExp)
)
{
var match = rx.Value.Match(s, 0);
if (!match.Success) // No match at all return the string in an array
{
a.DefineOwnProperty("0", new PropertyDescriptor(s, true, true, true), false);
return a;
}
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];
var 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();
if (!match.Success) // Add the last part of the split
{
a.DefineOwnProperty(index++.ToString(), new PropertyDescriptor(s.Substring(lastIndex), true, true, true), false);
}
}
return a;
}
else
{
var segments = new List();
var sep = TypeConverter.ToString(separator);
if (sep == string.Empty || (rx != null && rx.Source == regExpForMatchingAllCharactere)) // for s.split(new RegExp)
{
foreach (var c in s)
{
segments.Add(c.ToString());
}
}
else
{
segments = s.Split(new[] {sep}, StringSplitOptions.None).ToList();
}
for (int i = 0; i < segments.Count && i < limit; i++)
{
a.DefineOwnProperty(i.ToString(), new PropertyDescriptor(segments[i], true, true, true), false);
}
return a;
}
}
private JsValue Slice(JsValue thisObj, JsValue[] arguments)
{
TypeConverter.CheckObjectCoercible(Engine, thisObj);
var s = TypeConverter.ToString(thisObj);
var start = TypeConverter.ToNumber(arguments.At(0));
if (double.NegativeInfinity.Equals(start))
{
start = 0;
}
if (double.PositiveInfinity.Equals(start))
{
return string.Empty;
}
var end = TypeConverter.ToNumber(arguments.At(1));
if (double.PositiveInfinity.Equals(end))
{
end = s.Length;
}
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 JsValue Search(JsValue thisObj, JsValue[] arguments)
{
TypeConverter.CheckObjectCoercible(Engine, thisObj);
var s = TypeConverter.ToString(thisObj);
var regex = arguments.At(0);
if (regex.IsUndefined())
{
regex = string.Empty;
}
else if (regex.IsNull())
{
regex = Null.Text;
}
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 JsValue Replace(JsValue thisObj, JsValue[] arguments)
{
TypeConverter.CheckObjectCoercible(Engine, thisObj);
var thisString = TypeConverter.ToString(thisObj);
var searchValue = arguments.At(0);
var replaceValue = arguments.At(1);
// If the second parameter is not a function we create one
var replaceFunction = replaceValue.TryCast();
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 - 2)
{
// Two digit capture replacement.
replacementBuilder.Append(TypeConverter.ToString(args[matchNumber2]));
i++;
}
else if (matchNumber1 > 0 && matchNumber1 < args.Length - 2)
{
// Single digit capture replacement.
replacementBuilder.Append(TypeConverter.ToString(args[matchNumber1]));
}
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
if (searchValue.IsNull())
{
searchValue = new JsValue(Null.Text);
}
if (searchValue.IsUndefined())
{
searchValue = new JsValue(Undefined.Text);
}
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();
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);
var v = TypeConverter.ToString(replaceFunction.Call(Undefined.Instance, args.ToArray()));
return v;
}, 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 JsValue Match(JsValue thisObj, JsValue[] arguments)
{
TypeConverter.CheckObjectCoercible(Engine, thisObj);
var s = TypeConverter.ToString(thisObj);
var regex = arguments.At(0);
var rx = regex.TryCast();
rx = rx ?? (RegExpInstance) Engine.RegExp.Construct(new[] {regex});
var global = rx.Get("global").AsBoolean();
if (!global)
{
return Engine.RegExp.PrototypeObject.Exec(rx, Arguments.From(s));
}
else
{
rx.Put("lastIndex", 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)).TryCast();
if (result == null)
{
lastMatch = false;
}
else
{
var thisIndex = rx.Get("lastIndex").AsNumber();
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 JsValue LocaleCompare(JsValue thisObj, JsValue[] arguments)
{
TypeConverter.CheckObjectCoercible(Engine, thisObj);
var s = TypeConverter.ToString(thisObj);
var that = TypeConverter.ToString(arguments.At(0));
return string.CompareOrdinal(s, that);
}
private static List AllIndexesOf(string str, string value)
{
if (string.IsNullOrEmpty(value))
return new List();
var indexes = new List();
for (int index = 0; ; index += value.Length)
{
index = str.IndexOf(value, index);
if (index == -1) // no more fond
return indexes;
indexes.Add(index);
}
}
private int LastIndexJavaScriptImplementation(string s, string searchStr, int pos = -1)
{
if (pos == -1)
pos = s.Length;
var len = s.Length;
var start = System.Math.Min(System.Math.Max(pos, 0), len);
var searchLen = searchStr.Length;
var kPositions = AllIndexesOf(s, searchStr);
if (kPositions.Count == 0) // Nothing found
{
return -1;
}
else if (kPositions.Count == 1) // Only one found
{
return kPositions[0] <= start ? kPositions[0] : -1;
}
// Return the largest possible nonnegative integer k not larger than start
// such that k+ searchLen is not greater than len
for (var i = 0; i < kPositions.Count; i++)
{
if (kPositions[i] <= start)
{
// ok move to the next one to find a greater pos
}
else
{
if ((i > 0) && ((kPositions[i - 1] + searchLen) <= len))
return kPositions[i - 1];
else
return -1;
}
}
return kPositions[kPositions.Count - 1];
}
private JsValue LastIndexOf(JsValue thisObj, JsValue[] arguments)
{
TypeConverter.CheckObjectCoercible(Engine, thisObj);
var s = TypeConverter.ToString(thisObj);
var searchStr = TypeConverter.ToString(arguments.At(0));
double numPos = arguments.At(1) == Undefined.Instance ? s.Length : TypeConverter.ToNumber(arguments.At(1));
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));
// The JavaScript spec of string.lastIndexOf does match the C# spec
// Therefore we need to write our own specific implementation.
// Enjoy the fact that Ecma spec and Mozilla spec have different definition which
// I guess mean the same thing.
// Ecma spec
// http://www.ecma-international.org/ecma-262/5.1/#sec-15.5.4.8
// Mozilla spec
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/lastIndexOf
return LastIndexJavaScriptImplementation(s, searchStr, (int)start);
}
private JsValue IndexOf(JsValue thisObj, JsValue[] arguments)
{
TypeConverter.CheckObjectCoercible(Engine, thisObj);
var s = TypeConverter.ToString(thisObj);
var searchStr = TypeConverter.ToString(arguments.At(0));
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 JsValue Concat(JsValue thisObj, JsValue[] 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 JsValue CharCodeAt(JsValue thisObj, JsValue[] arguments)
{
TypeConverter.CheckObjectCoercible(Engine, thisObj);
JsValue 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 s[position];
}
private JsValue CharAt(JsValue thisObj, JsValue[] 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 JsValue ValueOf(JsValue thisObj, JsValue[] arguments)
{
var s = thisObj.TryCast();
if (s == null)
{
throw new JavaScriptException(Engine.TypeError);
}
return s.PrimitiveValue;
}
}
}