package otto import ( "bytes" "regexp" "strconv" "strings" "unicode/utf8" ) // String func stringValueFromStringArgumentList(argumentList []Value) Value { if len(argumentList) > 0 { return toValue_string(argumentList[0].string()) } return toValue_string("") } func builtinString(call FunctionCall) Value { return stringValueFromStringArgumentList(call.ArgumentList) } func builtinNewString(self *_object, argumentList []Value) Value { return toValue_object(self.runtime.newString(stringValueFromStringArgumentList(argumentList))) } func builtinString_toString(call FunctionCall) Value { return call.thisClassObject("String").primitiveValue() } func builtinString_valueOf(call FunctionCall) Value { return call.thisClassObject("String").primitiveValue() } func builtinString_fromCharCode(call FunctionCall) Value { chrList := make([]uint16, len(call.ArgumentList)) for index, value := range call.ArgumentList { chrList[index] = toUint16(value) } return toValue_string16(chrList) } func builtinString_charAt(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) idx := int(call.Argument(0).number().int64) chr := stringAt(call.This._object().stringValue(), idx) if chr == utf8.RuneError { return toValue_string("") } return toValue_string(string(chr)) } func builtinString_charCodeAt(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) idx := int(call.Argument(0).number().int64) chr := stringAt(call.This._object().stringValue(), idx) if chr == utf8.RuneError { return NaNValue() } return toValue_uint16(uint16(chr)) } func builtinString_concat(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) var value bytes.Buffer value.WriteString(call.This.string()) for _, item := range call.ArgumentList { value.WriteString(item.string()) } return toValue_string(value.String()) } func builtinString_indexOf(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) value := call.This.string() target := call.Argument(0).string() if 2 > len(call.ArgumentList) { return toValue_int(strings.Index(value, target)) } start := toIntegerFloat(call.Argument(1)) if 0 > start { start = 0 } else if start >= float64(len(value)) { if target == "" { return toValue_int(len(value)) } return toValue_int(-1) } index := strings.Index(value[int(start):], target) if index >= 0 { index += int(start) } return toValue_int(index) } func builtinString_lastIndexOf(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) value := call.This.string() target := call.Argument(0).string() if 2 > len(call.ArgumentList) || call.ArgumentList[1].IsUndefined() { return toValue_int(strings.LastIndex(value, target)) } length := len(value) if length == 0 { return toValue_int(strings.LastIndex(value, target)) } start := call.ArgumentList[1].number() if start.kind == numberInfinity { // FIXME // startNumber is infinity, so start is the end of string (start = length) return toValue_int(strings.LastIndex(value, target)) } if 0 > start.int64 { start.int64 = 0 } end := int(start.int64) + len(target) if end > length { end = length } return toValue_int(strings.LastIndex(value[:end], target)) } func builtinString_match(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) target := call.This.string() matcherValue := call.Argument(0) matcher := matcherValue._object() if !matcherValue.IsObject() || matcher.class != "RegExp" { matcher = call.runtime.newRegExp(matcherValue, Value{}) } global := matcher.get("global").bool() if !global { match, result := execRegExp(matcher, target) if !match { return nullValue } return toValue_object(execResultToArray(call.runtime, target, result)) } { result := matcher.regExpValue().regularExpression.FindAllStringIndex(target, -1) matchCount := len(result) if result == nil { matcher.put("lastIndex", toValue_int(0), true) return Value{} // !match } matchCount = len(result) valueArray := make([]Value, matchCount) for index := 0; index < matchCount; index++ { valueArray[index] = toValue_string(target[result[index][0]:result[index][1]]) } matcher.put("lastIndex", toValue_int(result[matchCount-1][1]), true) return toValue_object(call.runtime.newArrayOf(valueArray)) } } var builtinString_replace_Regexp = regexp.MustCompile("\\$(?:[\\$\\&\\'\\`1-9]|0[1-9]|[1-9][0-9])") func builtinString_findAndReplaceString(input []byte, lastIndex int, match []int, target []byte, replaceValue []byte) (output []byte) { matchCount := len(match) / 2 output = input if match[0] != lastIndex { output = append(output, target[lastIndex:match[0]]...) } replacement := builtinString_replace_Regexp.ReplaceAllFunc(replaceValue, func(part []byte) []byte { // TODO Check if match[0] or match[1] can be -1 in this scenario switch part[1] { case '$': return []byte{'$'} case '&': return target[match[0]:match[1]] case '`': return target[:match[0]] case '\'': return target[match[1]:len(target)] } matchNumberParse, error := strconv.ParseInt(string(part[1:]), 10, 64) matchNumber := int(matchNumberParse) if error != nil || matchNumber >= matchCount { return []byte{} } offset := 2 * matchNumber if match[offset] != -1 { return target[match[offset]:match[offset+1]] } return []byte{} // The empty string }) output = append(output, replacement...) return output } func builtinString_replace(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) target := []byte(call.This.string()) searchValue := call.Argument(0) searchObject := searchValue._object() // TODO If a capture is -1? var search *regexp.Regexp global := false find := 1 if searchValue.IsObject() && searchObject.class == "RegExp" { regExp := searchObject.regExpValue() search = regExp.regularExpression if regExp.global { find = -1 } } else { search = regexp.MustCompile(regexp.QuoteMeta(searchValue.string())) } found := search.FindAllSubmatchIndex(target, find) if found == nil { return toValue_string(string(target)) // !match } { lastIndex := 0 result := []byte{} replaceValue := call.Argument(1) if replaceValue.isCallable() { target := string(target) replace := replaceValue._object() for _, match := range found { if match[0] != lastIndex { result = append(result, target[lastIndex:match[0]]...) } matchCount := len(match) / 2 argumentList := make([]Value, matchCount+2) for index := 0; index < matchCount; index++ { offset := 2 * index if match[offset] != -1 { argumentList[index] = toValue_string(target[match[offset]:match[offset+1]]) } else { argumentList[index] = Value{} } } argumentList[matchCount+0] = toValue_int(match[0]) argumentList[matchCount+1] = toValue_string(target) replacement := replace.call(Value{}, argumentList, false, nativeFrame).string() result = append(result, []byte(replacement)...) lastIndex = match[1] } } else { replace := []byte(replaceValue.string()) for _, match := range found { result = builtinString_findAndReplaceString(result, lastIndex, match, target, replace) lastIndex = match[1] } } if lastIndex != len(target) { result = append(result, target[lastIndex:]...) } if global && searchObject != nil { searchObject.put("lastIndex", toValue_int(lastIndex), true) } return toValue_string(string(result)) } } func builtinString_search(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) target := call.This.string() searchValue := call.Argument(0) search := searchValue._object() if !searchValue.IsObject() || search.class != "RegExp" { search = call.runtime.newRegExp(searchValue, Value{}) } result := search.regExpValue().regularExpression.FindStringIndex(target) if result == nil { return toValue_int(-1) } return toValue_int(result[0]) } func stringSplitMatch(target string, targetLength int64, index uint, search string, searchLength int64) (bool, uint) { if int64(index)+searchLength > searchLength { return false, 0 } found := strings.Index(target[index:], search) if 0 > found { return false, 0 } return true, uint(found) } func builtinString_split(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) target := call.This.string() separatorValue := call.Argument(0) limitValue := call.Argument(1) limit := -1 if limitValue.IsDefined() { limit = int(toUint32(limitValue)) } if limit == 0 { return toValue_object(call.runtime.newArray(0)) } if separatorValue.IsUndefined() { return toValue_object(call.runtime.newArrayOf([]Value{toValue_string(target)})) } if separatorValue.isRegExp() { targetLength := len(target) search := separatorValue._object().regExpValue().regularExpression valueArray := []Value{} result := search.FindAllStringSubmatchIndex(target, -1) lastIndex := 0 found := 0 for _, match := range result { if match[0] == match[1] { // FIXME Ugh, this is a hack if match[0] == 0 || match[0] == targetLength { continue } } if lastIndex != match[0] { valueArray = append(valueArray, toValue_string(target[lastIndex:match[0]])) found++ } else if lastIndex == match[0] { if lastIndex != -1 { valueArray = append(valueArray, toValue_string("")) found++ } } lastIndex = match[1] if found == limit { goto RETURN } captureCount := len(match) / 2 for index := 1; index < captureCount; index++ { offset := index * 2 value := Value{} if match[offset] != -1 { value = toValue_string(target[match[offset]:match[offset+1]]) } valueArray = append(valueArray, value) found++ if found == limit { goto RETURN } } } if found != limit { if lastIndex != targetLength { valueArray = append(valueArray, toValue_string(target[lastIndex:targetLength])) } else { valueArray = append(valueArray, toValue_string("")) } } RETURN: return toValue_object(call.runtime.newArrayOf(valueArray)) } else { separator := separatorValue.string() splitLimit := limit excess := false if limit > 0 { splitLimit = limit + 1 excess = true } split := strings.SplitN(target, separator, splitLimit) if excess && len(split) > limit { split = split[:limit] } return call.runtime.toValue(split) } } func builtinString_slice(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) target := call.This.string() length := int64(len(target)) start, end := rangeStartEnd(call.ArgumentList, length, false) if end-start <= 0 { return toValue_string("") } return toValue_string(target[start:end]) } func builtinString_substring(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) target := call.This.string() length := int64(len(target)) start, end := rangeStartEnd(call.ArgumentList, length, true) if start > end { start, end = end, start } return toValue_string(target[start:end]) } func builtinString_substr(call FunctionCall) Value { target := call.This.string() size := int64(len(target)) start, length := rangeStartLength(call.ArgumentList, size) if start >= size { return toValue_string("") } if length <= 0 { return toValue_string("") } if start+length >= size { // Cap length to be to the end of the string // start = 3, length = 5, size = 4 [0, 1, 2, 3] // 4 - 3 = 1 // target[3:4] length = size - start } return toValue_string(target[start : start+length]) } func builtinString_toLowerCase(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) return toValue_string(strings.ToLower(call.This.string())) } func builtinString_toUpperCase(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) return toValue_string(strings.ToUpper(call.This.string())) } // 7.2 Table 2 — Whitespace Characters & 7.3 Table 3 - Line Terminator Characters const builtinString_trim_whitespace = "\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF" func builtinString_trim(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) return toValue(strings.Trim(call.This.string(), builtinString_trim_whitespace)) } // Mozilla extension, not ECMAScript 5 func builtinString_trimLeft(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) return toValue(strings.TrimLeft(call.This.string(), builtinString_trim_whitespace)) } // Mozilla extension, not ECMAScript 5 func builtinString_trimRight(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) return toValue(strings.TrimRight(call.This.string(), builtinString_trim_whitespace)) } func builtinString_localeCompare(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) this := call.This.string() that := call.Argument(0).string() if this < that { return toValue_int(-1) } else if this == that { return toValue_int(0) } return toValue_int(1) } /* An alternate version of String.trim func builtinString_trim(call FunctionCall) Value { checkObjectCoercible(call.This) return toValue_string(strings.TrimFunc(call.string(.This), isWhiteSpaceOrLineTerminator)) } */ func builtinString_toLocaleLowerCase(call FunctionCall) Value { return builtinString_toLowerCase(call) } func builtinString_toLocaleUpperCase(call FunctionCall) Value { return builtinString_toUpperCase(call) }