688 lines
16 KiB
Go
688 lines
16 KiB
Go
package goja
|
|
|
|
import (
|
|
"bytes"
|
|
"github.com/dop251/goja/parser"
|
|
"golang.org/x/text/collate"
|
|
"golang.org/x/text/language"
|
|
"golang.org/x/text/unicode/norm"
|
|
"math"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
var (
|
|
collator = collate.New(language.Und)
|
|
)
|
|
|
|
func (r *Runtime) builtin_String(call FunctionCall) Value {
|
|
if len(call.Arguments) > 0 {
|
|
arg := call.Arguments[0]
|
|
if _, ok := arg.assertString(); ok {
|
|
return arg
|
|
}
|
|
return arg.ToString()
|
|
} else {
|
|
return newStringValue("")
|
|
}
|
|
}
|
|
|
|
func (r *Runtime) _newString(s valueString) *Object {
|
|
v := &Object{runtime: r}
|
|
|
|
o := &stringObject{}
|
|
o.class = classString
|
|
o.val = v
|
|
o.extensible = true
|
|
v.self = o
|
|
o.prototype = r.global.StringPrototype
|
|
if s != nil {
|
|
o.value = s
|
|
}
|
|
o.init()
|
|
return v
|
|
}
|
|
|
|
func (r *Runtime) builtin_newString(args []Value) *Object {
|
|
var s valueString
|
|
if len(args) > 0 {
|
|
s = args[0].ToString()
|
|
} else {
|
|
s = stringEmpty
|
|
}
|
|
return r._newString(s)
|
|
}
|
|
|
|
func searchSubstringUTF8(str, search string) (ret [][]int) {
|
|
searchPos := 0
|
|
l := len(str)
|
|
if searchPos < l {
|
|
p := strings.Index(str[searchPos:], search)
|
|
if p != -1 {
|
|
p += searchPos
|
|
searchPos = p + len(search)
|
|
ret = append(ret, []int{p, searchPos})
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (r *Runtime) stringproto_toStringValueOf(this Value, funcName string) Value {
|
|
if str, ok := this.assertString(); ok {
|
|
return str
|
|
}
|
|
if obj, ok := this.(*Object); ok {
|
|
if strObj, ok := obj.self.(*stringObject); ok {
|
|
return strObj.value
|
|
}
|
|
}
|
|
r.typeErrorResult(true, "String.prototype.%s is called on incompatible receiver", funcName)
|
|
return nil
|
|
}
|
|
|
|
func (r *Runtime) stringproto_toString(call FunctionCall) Value {
|
|
return r.stringproto_toStringValueOf(call.This, "toString")
|
|
}
|
|
|
|
func (r *Runtime) stringproto_valueOf(call FunctionCall) Value {
|
|
return r.stringproto_toStringValueOf(call.This, "valueOf")
|
|
}
|
|
|
|
func (r *Runtime) string_fromcharcode(call FunctionCall) Value {
|
|
b := make([]byte, len(call.Arguments))
|
|
for i, arg := range call.Arguments {
|
|
chr := toUInt16(arg)
|
|
if chr >= utf8.RuneSelf {
|
|
bb := make([]uint16, len(call.Arguments))
|
|
for j := 0; j < i; j++ {
|
|
bb[j] = uint16(b[j])
|
|
}
|
|
bb[i] = chr
|
|
i++
|
|
for j, arg := range call.Arguments[i:] {
|
|
bb[i+j] = toUInt16(arg)
|
|
}
|
|
return unicodeString(bb)
|
|
}
|
|
b[i] = byte(chr)
|
|
}
|
|
|
|
return asciiString(b)
|
|
}
|
|
|
|
func (r *Runtime) stringproto_charAt(call FunctionCall) Value {
|
|
r.checkObjectCoercible(call.This)
|
|
s := call.This.ToString()
|
|
pos := call.Argument(0).ToInteger()
|
|
if pos < 0 || pos >= s.length() {
|
|
return stringEmpty
|
|
}
|
|
return newStringValue(string(s.charAt(pos)))
|
|
}
|
|
|
|
func (r *Runtime) stringproto_charCodeAt(call FunctionCall) Value {
|
|
r.checkObjectCoercible(call.This)
|
|
s := call.This.ToString()
|
|
pos := call.Argument(0).ToInteger()
|
|
if pos < 0 || pos >= s.length() {
|
|
return _NaN
|
|
}
|
|
return intToValue(int64(s.charAt(pos) & 0xFFFF))
|
|
}
|
|
|
|
func (r *Runtime) stringproto_concat(call FunctionCall) Value {
|
|
r.checkObjectCoercible(call.This)
|
|
strs := make([]valueString, len(call.Arguments)+1)
|
|
strs[0] = call.This.ToString()
|
|
_, allAscii := strs[0].(asciiString)
|
|
totalLen := strs[0].length()
|
|
for i, arg := range call.Arguments {
|
|
s := arg.ToString()
|
|
if allAscii {
|
|
_, allAscii = s.(asciiString)
|
|
}
|
|
strs[i+1] = s
|
|
totalLen += s.length()
|
|
}
|
|
|
|
if allAscii {
|
|
buf := bytes.NewBuffer(make([]byte, 0, totalLen))
|
|
for _, s := range strs {
|
|
buf.WriteString(s.String())
|
|
}
|
|
return asciiString(buf.String())
|
|
} else {
|
|
buf := make([]uint16, totalLen)
|
|
pos := int64(0)
|
|
for _, s := range strs {
|
|
switch s := s.(type) {
|
|
case asciiString:
|
|
for i := 0; i < len(s); i++ {
|
|
buf[pos] = uint16(s[i])
|
|
pos++
|
|
}
|
|
case unicodeString:
|
|
copy(buf[pos:], s)
|
|
pos += s.length()
|
|
}
|
|
}
|
|
return unicodeString(buf)
|
|
}
|
|
}
|
|
|
|
func (r *Runtime) stringproto_indexOf(call FunctionCall) Value {
|
|
r.checkObjectCoercible(call.This)
|
|
value := call.This.ToString()
|
|
target := call.Argument(0).ToString()
|
|
pos := call.Argument(1).ToInteger()
|
|
|
|
if pos < 0 {
|
|
pos = 0
|
|
} else {
|
|
l := value.length()
|
|
if pos > l {
|
|
pos = l
|
|
}
|
|
}
|
|
|
|
return intToValue(value.index(target, pos))
|
|
}
|
|
|
|
func (r *Runtime) stringproto_lastIndexOf(call FunctionCall) Value {
|
|
r.checkObjectCoercible(call.This)
|
|
value := call.This.ToString()
|
|
target := call.Argument(0).ToString()
|
|
numPos := call.Argument(1).ToNumber()
|
|
|
|
var pos int64
|
|
if f, ok := numPos.assertFloat(); ok && math.IsNaN(f) {
|
|
pos = value.length()
|
|
} else {
|
|
pos = numPos.ToInteger()
|
|
if pos < 0 {
|
|
pos = 0
|
|
} else {
|
|
l := value.length()
|
|
if pos > l {
|
|
pos = l
|
|
}
|
|
}
|
|
}
|
|
|
|
return intToValue(value.lastIndex(target, pos))
|
|
}
|
|
|
|
func (r *Runtime) stringproto_localeCompare(call FunctionCall) Value {
|
|
r.checkObjectCoercible(call.This)
|
|
this := norm.NFD.String(call.This.String())
|
|
that := norm.NFD.String(call.Argument(0).String())
|
|
return intToValue(int64(collator.CompareString(this, that)))
|
|
}
|
|
|
|
func (r *Runtime) stringproto_match(call FunctionCall) Value {
|
|
r.checkObjectCoercible(call.This)
|
|
s := call.This.ToString()
|
|
regexp := call.Argument(0)
|
|
var rx *regexpObject
|
|
if regexp, ok := regexp.(*Object); ok {
|
|
rx, _ = regexp.self.(*regexpObject)
|
|
}
|
|
|
|
if rx == nil {
|
|
rx = r.builtin_newRegExp([]Value{regexp}).self.(*regexpObject)
|
|
}
|
|
|
|
if rx.global {
|
|
rx.putStr("lastIndex", intToValue(0), false)
|
|
var a []Value
|
|
var previousLastIndex int64
|
|
for {
|
|
match, result := rx.execRegexp(s)
|
|
if !match {
|
|
break
|
|
}
|
|
thisIndex := rx.getStr("lastIndex").ToInteger()
|
|
if thisIndex == previousLastIndex {
|
|
previousLastIndex++
|
|
rx.putStr("lastIndex", intToValue(previousLastIndex), false)
|
|
} else {
|
|
previousLastIndex = thisIndex
|
|
}
|
|
a = append(a, s.substring(int64(result[0]), int64(result[1])))
|
|
}
|
|
if len(a) == 0 {
|
|
return _null
|
|
}
|
|
return r.newArrayValues(a)
|
|
} else {
|
|
return rx.exec(s)
|
|
}
|
|
}
|
|
|
|
func (r *Runtime) stringproto_replace(call FunctionCall) Value {
|
|
s := call.This.ToString()
|
|
var str string
|
|
var isASCII bool
|
|
if astr, ok := s.(asciiString); ok {
|
|
str = string(astr)
|
|
isASCII = true
|
|
} else {
|
|
str = s.String()
|
|
}
|
|
searchValue := call.Argument(0)
|
|
replaceValue := call.Argument(1)
|
|
|
|
var found [][]int
|
|
|
|
if searchValue, ok := searchValue.(*Object); ok {
|
|
if regexp, ok := searchValue.self.(*regexpObject); ok {
|
|
find := 1
|
|
if regexp.global {
|
|
find = -1
|
|
}
|
|
if isASCII {
|
|
found = regexp.pattern.FindAllSubmatchIndexASCII(str, find)
|
|
} else {
|
|
found = regexp.pattern.FindAllSubmatchIndexUTF8(str, find)
|
|
}
|
|
if found == nil {
|
|
return s
|
|
}
|
|
}
|
|
}
|
|
|
|
if found == nil {
|
|
found = searchSubstringUTF8(str, searchValue.String())
|
|
}
|
|
|
|
if len(found) == 0 {
|
|
return s
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
lastIndex := 0
|
|
|
|
var rcall func(FunctionCall) Value
|
|
|
|
if replaceValue, ok := replaceValue.(*Object); ok {
|
|
if c, ok := replaceValue.self.assertCallable(); ok {
|
|
rcall = c
|
|
}
|
|
}
|
|
|
|
if rcall != nil {
|
|
for _, item := range found {
|
|
if item[0] != lastIndex {
|
|
buf.WriteString(str[lastIndex:item[0]])
|
|
}
|
|
matchCount := len(item) / 2
|
|
argumentList := make([]Value, matchCount+2)
|
|
for index := 0; index < matchCount; index++ {
|
|
offset := 2 * index
|
|
if item[offset] != -1 {
|
|
if isASCII {
|
|
argumentList[index] = asciiString(str[item[offset]:item[offset+1]])
|
|
} else {
|
|
argumentList[index] = newStringValue(str[item[offset]:item[offset+1]])
|
|
}
|
|
} else {
|
|
argumentList[index] = _undefined
|
|
}
|
|
}
|
|
argumentList[matchCount] = valueInt(item[0])
|
|
argumentList[matchCount+1] = s
|
|
replacement := rcall(FunctionCall{
|
|
This: _undefined,
|
|
Arguments: argumentList,
|
|
}).String()
|
|
buf.WriteString(replacement)
|
|
lastIndex = item[1]
|
|
}
|
|
} else {
|
|
newstring := replaceValue.String()
|
|
|
|
for _, item := range found {
|
|
if item[0] != lastIndex {
|
|
buf.WriteString(str[lastIndex:item[0]])
|
|
}
|
|
matches := len(item) / 2
|
|
for i := 0; i < len(newstring); i++ {
|
|
if newstring[i] == '$' && i < len(newstring)-1 {
|
|
ch := newstring[i+1]
|
|
switch ch {
|
|
case '$':
|
|
buf.WriteByte('$')
|
|
case '`':
|
|
buf.WriteString(str[0:item[0]])
|
|
case '\'':
|
|
buf.WriteString(str[item[1]:])
|
|
case '&':
|
|
buf.WriteString(str[item[0]:item[1]])
|
|
default:
|
|
matchNumber := 0
|
|
l := 0
|
|
for _, ch := range newstring[i+1:] {
|
|
if ch >= '0' && ch <= '9' {
|
|
m := matchNumber*10 + int(ch-'0')
|
|
if m >= matches {
|
|
break
|
|
}
|
|
matchNumber = m
|
|
l++
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
if l > 0 {
|
|
offset := 2 * matchNumber
|
|
if offset < len(item) && item[offset] != -1 {
|
|
buf.WriteString(str[item[offset]:item[offset+1]])
|
|
}
|
|
i += l - 1
|
|
} else {
|
|
buf.WriteByte('$')
|
|
buf.WriteByte(ch)
|
|
}
|
|
|
|
}
|
|
i++
|
|
} else {
|
|
buf.WriteByte(newstring[i])
|
|
}
|
|
}
|
|
lastIndex = item[1]
|
|
}
|
|
}
|
|
|
|
if lastIndex != len(str) {
|
|
buf.WriteString(str[lastIndex:])
|
|
}
|
|
|
|
return newStringValue(buf.String())
|
|
}
|
|
|
|
func (r *Runtime) stringproto_search(call FunctionCall) Value {
|
|
r.checkObjectCoercible(call.This)
|
|
s := call.This.ToString()
|
|
regexp := call.Argument(0)
|
|
var rx *regexpObject
|
|
if regexp, ok := regexp.(*Object); ok {
|
|
rx, _ = regexp.self.(*regexpObject)
|
|
}
|
|
|
|
if rx == nil {
|
|
rx = r.builtin_newRegExp([]Value{regexp}).self.(*regexpObject)
|
|
}
|
|
|
|
match, result := rx.execRegexp(s)
|
|
if !match {
|
|
return intToValue(-1)
|
|
}
|
|
return intToValue(int64(result[0]))
|
|
}
|
|
|
|
func (r *Runtime) stringproto_slice(call FunctionCall) Value {
|
|
r.checkObjectCoercible(call.This)
|
|
s := call.This.ToString()
|
|
|
|
l := s.length()
|
|
start := call.Argument(0).ToInteger()
|
|
var end int64
|
|
if arg1 := call.Argument(1); arg1 != _undefined {
|
|
end = arg1.ToInteger()
|
|
} else {
|
|
end = l
|
|
}
|
|
|
|
if start < 0 {
|
|
start += l
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
} else {
|
|
if start > l {
|
|
start = l
|
|
}
|
|
}
|
|
|
|
if end < 0 {
|
|
end += l
|
|
if end < 0 {
|
|
end = 0
|
|
}
|
|
} else {
|
|
if end > l {
|
|
end = l
|
|
}
|
|
}
|
|
|
|
if end > start {
|
|
return s.substring(start, end)
|
|
}
|
|
return stringEmpty
|
|
}
|
|
|
|
func (r *Runtime) stringproto_split(call FunctionCall) Value {
|
|
r.checkObjectCoercible(call.This)
|
|
s := call.This.ToString()
|
|
|
|
separatorValue := call.Argument(0)
|
|
limitValue := call.Argument(1)
|
|
limit := -1
|
|
if limitValue != _undefined {
|
|
limit = int(toUInt32(limitValue))
|
|
}
|
|
|
|
if limit == 0 {
|
|
return r.newArrayValues(nil)
|
|
}
|
|
|
|
if separatorValue == _undefined {
|
|
return r.newArrayValues([]Value{s})
|
|
}
|
|
|
|
var search *regexpObject
|
|
if o, ok := separatorValue.(*Object); ok {
|
|
search, _ = o.self.(*regexpObject)
|
|
}
|
|
|
|
if search != nil {
|
|
targetLength := s.length()
|
|
valueArray := []Value{}
|
|
result := search.pattern.FindAllSubmatchIndex(s, -1)
|
|
lastIndex := 0
|
|
found := 0
|
|
|
|
for _, match := range result {
|
|
if match[0] == match[1] {
|
|
// FIXME Ugh, this is a hack
|
|
if match[0] == 0 || int64(match[0]) == targetLength {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if lastIndex != match[0] {
|
|
valueArray = append(valueArray, s.substring(int64(lastIndex), int64(match[0])))
|
|
found++
|
|
} else if lastIndex == match[0] {
|
|
if lastIndex != -1 {
|
|
valueArray = append(valueArray, stringEmpty)
|
|
found++
|
|
}
|
|
}
|
|
|
|
lastIndex = match[1]
|
|
if found == limit {
|
|
goto RETURN
|
|
}
|
|
|
|
captureCount := len(match) / 2
|
|
for index := 1; index < captureCount; index++ {
|
|
offset := index * 2
|
|
var value Value
|
|
if match[offset] != -1 {
|
|
value = s.substring(int64(match[offset]), int64(match[offset+1]))
|
|
} else {
|
|
value = _undefined
|
|
}
|
|
valueArray = append(valueArray, value)
|
|
found++
|
|
if found == limit {
|
|
goto RETURN
|
|
}
|
|
}
|
|
}
|
|
|
|
if found != limit {
|
|
if int64(lastIndex) != targetLength {
|
|
valueArray = append(valueArray, s.substring(int64(lastIndex), targetLength))
|
|
} else {
|
|
valueArray = append(valueArray, stringEmpty)
|
|
}
|
|
}
|
|
|
|
RETURN:
|
|
return r.newArrayValues(valueArray)
|
|
|
|
} else {
|
|
separator := separatorValue.String()
|
|
|
|
excess := false
|
|
str := s.String()
|
|
if limit > len(str) {
|
|
limit = len(str)
|
|
}
|
|
splitLimit := limit
|
|
if limit > 0 {
|
|
splitLimit = limit + 1
|
|
excess = true
|
|
}
|
|
|
|
split := strings.SplitN(str, separator, splitLimit)
|
|
|
|
if excess && len(split) > limit {
|
|
split = split[:limit]
|
|
}
|
|
|
|
valueArray := make([]Value, len(split))
|
|
for index, value := range split {
|
|
valueArray[index] = newStringValue(value)
|
|
}
|
|
|
|
return r.newArrayValues(valueArray)
|
|
}
|
|
|
|
}
|
|
|
|
func (r *Runtime) stringproto_substring(call FunctionCall) Value {
|
|
r.checkObjectCoercible(call.This)
|
|
s := call.This.ToString()
|
|
|
|
l := s.length()
|
|
intStart := call.Argument(0).ToInteger()
|
|
var intEnd int64
|
|
if end := call.Argument(1); end != _undefined {
|
|
intEnd = end.ToInteger()
|
|
} else {
|
|
intEnd = l
|
|
}
|
|
if intStart < 0 {
|
|
intStart = 0
|
|
} else if intStart > l {
|
|
intStart = l
|
|
}
|
|
|
|
if intEnd < 0 {
|
|
intEnd = 0
|
|
} else if intEnd > l {
|
|
intEnd = l
|
|
}
|
|
|
|
if intStart > intEnd {
|
|
intStart, intEnd = intEnd, intStart
|
|
}
|
|
|
|
return s.substring(intStart, intEnd)
|
|
}
|
|
|
|
func (r *Runtime) stringproto_toLowerCase(call FunctionCall) Value {
|
|
r.checkObjectCoercible(call.This)
|
|
s := call.This.ToString()
|
|
|
|
return s.toLower()
|
|
}
|
|
|
|
func (r *Runtime) stringproto_toUpperCase(call FunctionCall) Value {
|
|
r.checkObjectCoercible(call.This)
|
|
s := call.This.ToString()
|
|
|
|
return s.toUpper()
|
|
}
|
|
|
|
func (r *Runtime) stringproto_trim(call FunctionCall) Value {
|
|
r.checkObjectCoercible(call.This)
|
|
s := call.This.ToString()
|
|
|
|
return newStringValue(strings.Trim(s.String(), parser.WhitespaceChars))
|
|
}
|
|
|
|
func (r *Runtime) stringproto_substr(call FunctionCall) Value {
|
|
s := call.This.ToString()
|
|
start := call.Argument(0).ToInteger()
|
|
var length int64
|
|
sl := int64(s.length())
|
|
if arg := call.Argument(1); arg != _undefined {
|
|
length = arg.ToInteger()
|
|
} else {
|
|
length = sl
|
|
}
|
|
|
|
if start < 0 {
|
|
start = max(sl+start, 0)
|
|
}
|
|
|
|
length = min(max(length, 0), sl-start)
|
|
if length <= 0 {
|
|
return stringEmpty
|
|
}
|
|
|
|
return s.substring(start, start+length)
|
|
}
|
|
|
|
func (r *Runtime) initString() {
|
|
r.global.StringPrototype = r.builtin_newString([]Value{stringEmpty})
|
|
|
|
o := r.global.StringPrototype.self
|
|
o.(*stringObject).prototype = r.global.ObjectPrototype
|
|
o._putProp("toString", r.newNativeFunc(r.stringproto_toString, nil, "toString", nil, 0), true, false, true)
|
|
o._putProp("valueOf", r.newNativeFunc(r.stringproto_valueOf, nil, "valueOf", nil, 0), true, false, true)
|
|
o._putProp("charAt", r.newNativeFunc(r.stringproto_charAt, nil, "charAt", nil, 1), true, false, true)
|
|
o._putProp("charCodeAt", r.newNativeFunc(r.stringproto_charCodeAt, nil, "charCodeAt", nil, 1), true, false, true)
|
|
o._putProp("concat", r.newNativeFunc(r.stringproto_concat, nil, "concat", nil, 1), true, false, true)
|
|
o._putProp("indexOf", r.newNativeFunc(r.stringproto_indexOf, nil, "indexOf", nil, 1), true, false, true)
|
|
o._putProp("lastIndexOf", r.newNativeFunc(r.stringproto_lastIndexOf, nil, "lastIndexOf", nil, 1), true, false, true)
|
|
o._putProp("localeCompare", r.newNativeFunc(r.stringproto_localeCompare, nil, "localeCompare", nil, 1), true, false, true)
|
|
o._putProp("match", r.newNativeFunc(r.stringproto_match, nil, "match", nil, 1), true, false, true)
|
|
o._putProp("replace", r.newNativeFunc(r.stringproto_replace, nil, "replace", nil, 2), true, false, true)
|
|
o._putProp("search", r.newNativeFunc(r.stringproto_search, nil, "search", nil, 1), true, false, true)
|
|
o._putProp("slice", r.newNativeFunc(r.stringproto_slice, nil, "slice", nil, 2), true, false, true)
|
|
o._putProp("split", r.newNativeFunc(r.stringproto_split, nil, "split", nil, 2), true, false, true)
|
|
o._putProp("substring", r.newNativeFunc(r.stringproto_substring, nil, "substring", nil, 2), true, false, true)
|
|
o._putProp("toLowerCase", r.newNativeFunc(r.stringproto_toLowerCase, nil, "toLowerCase", nil, 0), true, false, true)
|
|
o._putProp("toLocaleLowerCase", r.newNativeFunc(r.stringproto_toLowerCase, nil, "toLocaleLowerCase", nil, 0), true, false, true)
|
|
o._putProp("toUpperCase", r.newNativeFunc(r.stringproto_toUpperCase, nil, "toUpperCase", nil, 0), true, false, true)
|
|
o._putProp("toLocaleUpperCase", r.newNativeFunc(r.stringproto_toUpperCase, nil, "toLocaleUpperCase", nil, 0), true, false, true)
|
|
o._putProp("trim", r.newNativeFunc(r.stringproto_trim, nil, "trim", nil, 0), true, false, true)
|
|
|
|
// Annex B
|
|
o._putProp("substr", r.newNativeFunc(r.stringproto_substr, nil, "substr", nil, 2), true, false, true)
|
|
|
|
r.global.String = r.newNativeFunc(r.builtin_String, r.builtin_newString, "String", r.global.StringPrototype, 1)
|
|
o = r.global.String.self
|
|
o._putProp("fromCharCode", r.newNativeFunc(r.string_fromcharcode, nil, "fromCharCode", nil, 1), true, false, true)
|
|
|
|
r.addToGlobal("String", r.global.String)
|
|
|
|
r.stringSingleton = r.builtin_new(r.global.String, nil).self.(*stringObject)
|
|
}
|