298 lines
7.5 KiB
Go
298 lines
7.5 KiB
Go
package otto
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
type builtinJSONParseContext struct {
|
|
call FunctionCall
|
|
reviver Value
|
|
}
|
|
|
|
func builtinJSONParse(call FunctionCall) Value {
|
|
ctx := builtinJSONParseContext{
|
|
call: call,
|
|
}
|
|
revive := false
|
|
if reviver := call.Argument(1); reviver.isCallable() {
|
|
revive = true
|
|
ctx.reviver = reviver
|
|
}
|
|
|
|
var root interface{}
|
|
err := json.Unmarshal([]byte(call.Argument(0).string()), &root)
|
|
if err != nil {
|
|
panic(call.runtime.panicSyntaxError(err.Error()))
|
|
}
|
|
value, exists := builtinJSONParseWalk(ctx, root)
|
|
if !exists {
|
|
value = Value{}
|
|
}
|
|
if revive {
|
|
root := ctx.call.runtime.newObject()
|
|
root.put("", value, false)
|
|
return builtinJSONReviveWalk(ctx, root, "")
|
|
}
|
|
return value
|
|
}
|
|
|
|
func builtinJSONReviveWalk(ctx builtinJSONParseContext, holder *object, name string) Value {
|
|
value := holder.get(name)
|
|
if obj := value.object(); obj != nil {
|
|
if isArray(obj) {
|
|
length := int64(objectLength(obj))
|
|
for index := int64(0); index < length; index++ {
|
|
name := arrayIndexToString(index)
|
|
value := builtinJSONReviveWalk(ctx, obj, name)
|
|
if value.IsUndefined() {
|
|
obj.delete(name, false)
|
|
} else {
|
|
obj.defineProperty(name, value, 0o111, false)
|
|
}
|
|
}
|
|
} else {
|
|
obj.enumerate(false, func(name string) bool {
|
|
value := builtinJSONReviveWalk(ctx, obj, name)
|
|
if value.IsUndefined() {
|
|
obj.delete(name, false)
|
|
} else {
|
|
obj.defineProperty(name, value, 0o111, false)
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
}
|
|
return ctx.reviver.call(ctx.call.runtime, objectValue(holder), name, value)
|
|
}
|
|
|
|
func builtinJSONParseWalk(ctx builtinJSONParseContext, rawValue interface{}) (Value, bool) {
|
|
switch value := rawValue.(type) {
|
|
case nil:
|
|
return nullValue, true
|
|
case bool:
|
|
return boolValue(value), true
|
|
case string:
|
|
return stringValue(value), true
|
|
case float64:
|
|
return float64Value(value), true
|
|
case []interface{}:
|
|
arrayValue := make([]Value, len(value))
|
|
for index, rawValue := range value {
|
|
if value, exists := builtinJSONParseWalk(ctx, rawValue); exists {
|
|
arrayValue[index] = value
|
|
}
|
|
}
|
|
return objectValue(ctx.call.runtime.newArrayOf(arrayValue)), true
|
|
case map[string]interface{}:
|
|
obj := ctx.call.runtime.newObject()
|
|
for name, rawValue := range value {
|
|
if value, exists := builtinJSONParseWalk(ctx, rawValue); exists {
|
|
obj.put(name, value, false)
|
|
}
|
|
}
|
|
return objectValue(obj), true
|
|
}
|
|
return Value{}, false
|
|
}
|
|
|
|
type builtinJSONStringifyContext struct {
|
|
call FunctionCall
|
|
stack []*object
|
|
propertyList []string
|
|
replacerFunction *Value
|
|
gap string
|
|
}
|
|
|
|
func builtinJSONStringify(call FunctionCall) Value {
|
|
ctx := builtinJSONStringifyContext{
|
|
call: call,
|
|
stack: []*object{nil},
|
|
}
|
|
replacer := call.Argument(1).object()
|
|
if replacer != nil {
|
|
if isArray(replacer) {
|
|
length := objectLength(replacer)
|
|
seen := map[string]bool{}
|
|
propertyList := make([]string, length)
|
|
length = 0
|
|
for index := range propertyList {
|
|
value := replacer.get(arrayIndexToString(int64(index)))
|
|
switch value.kind {
|
|
case valueObject:
|
|
switch value.value.(*object).class {
|
|
case classStringName, classNumberName:
|
|
default:
|
|
continue
|
|
}
|
|
case valueString, valueNumber:
|
|
default:
|
|
continue
|
|
}
|
|
name := value.string()
|
|
if seen[name] {
|
|
continue
|
|
}
|
|
seen[name] = true
|
|
length++
|
|
propertyList[index] = name
|
|
}
|
|
ctx.propertyList = propertyList[0:length]
|
|
} else if replacer.class == classFunctionName {
|
|
value := objectValue(replacer)
|
|
ctx.replacerFunction = &value
|
|
}
|
|
}
|
|
if spaceValue, exists := call.getArgument(2); exists {
|
|
if spaceValue.kind == valueObject {
|
|
switch spaceValue.value.(*object).class {
|
|
case classStringName:
|
|
spaceValue = stringValue(spaceValue.string())
|
|
case classNumberName:
|
|
spaceValue = spaceValue.numberValue()
|
|
}
|
|
}
|
|
switch spaceValue.kind {
|
|
case valueString:
|
|
value := spaceValue.string()
|
|
if len(value) > 10 {
|
|
ctx.gap = value[0:10]
|
|
} else {
|
|
ctx.gap = value
|
|
}
|
|
case valueNumber:
|
|
value := spaceValue.number().int64
|
|
if value > 10 {
|
|
value = 10
|
|
} else if value < 0 {
|
|
value = 0
|
|
}
|
|
ctx.gap = strings.Repeat(" ", int(value))
|
|
}
|
|
}
|
|
holder := call.runtime.newObject()
|
|
holder.put("", call.Argument(0), false)
|
|
value, exists := builtinJSONStringifyWalk(ctx, "", holder)
|
|
if !exists {
|
|
return Value{}
|
|
}
|
|
valueJSON, err := json.Marshal(value)
|
|
if err != nil {
|
|
panic(call.runtime.panicTypeError("JSON.stringify marshal: %s", err))
|
|
}
|
|
if ctx.gap != "" {
|
|
valueJSON1 := bytes.Buffer{}
|
|
if err = json.Indent(&valueJSON1, valueJSON, "", ctx.gap); err != nil {
|
|
panic(call.runtime.panicTypeError("JSON.stringify indent: %s", err))
|
|
}
|
|
valueJSON = valueJSON1.Bytes()
|
|
}
|
|
return stringValue(string(valueJSON))
|
|
}
|
|
|
|
func builtinJSONStringifyWalk(ctx builtinJSONStringifyContext, key string, holder *object) (interface{}, bool) {
|
|
value := holder.get(key)
|
|
|
|
if value.IsObject() {
|
|
obj := value.object()
|
|
if toJSON := obj.get("toJSON"); toJSON.IsFunction() {
|
|
value = toJSON.call(ctx.call.runtime, value, key)
|
|
} else if obj.objectClass.marshalJSON != nil {
|
|
// If the object is a GoStruct or something that implements json.Marshaler
|
|
marshaler := obj.objectClass.marshalJSON(obj)
|
|
if marshaler != nil {
|
|
return marshaler, true
|
|
}
|
|
}
|
|
}
|
|
|
|
if ctx.replacerFunction != nil {
|
|
value = ctx.replacerFunction.call(ctx.call.runtime, objectValue(holder), key, value)
|
|
}
|
|
|
|
if value.kind == valueObject {
|
|
switch value.value.(*object).class {
|
|
case classBooleanName:
|
|
value = value.object().value.(Value)
|
|
case classStringName:
|
|
value = stringValue(value.string())
|
|
case classNumberName:
|
|
value = value.numberValue()
|
|
}
|
|
}
|
|
|
|
switch value.kind {
|
|
case valueBoolean:
|
|
return value.bool(), true
|
|
case valueString:
|
|
return value.string(), true
|
|
case valueNumber:
|
|
integer := value.number()
|
|
switch integer.kind {
|
|
case numberInteger:
|
|
return integer.int64, true
|
|
case numberFloat:
|
|
return integer.float64, true
|
|
default:
|
|
return nil, true
|
|
}
|
|
case valueNull:
|
|
return nil, true
|
|
case valueObject:
|
|
holder := value.object()
|
|
if value := value.object(); nil != value {
|
|
for _, obj := range ctx.stack {
|
|
if holder == obj {
|
|
panic(ctx.call.runtime.panicTypeError("Converting circular structure to JSON"))
|
|
}
|
|
}
|
|
ctx.stack = append(ctx.stack, value)
|
|
defer func() { ctx.stack = ctx.stack[:len(ctx.stack)-1] }()
|
|
}
|
|
if isArray(holder) {
|
|
var length uint32
|
|
switch value := holder.get(propertyLength).value.(type) {
|
|
case uint32:
|
|
length = value
|
|
case int:
|
|
if value >= 0 {
|
|
length = uint32(value)
|
|
}
|
|
default:
|
|
panic(ctx.call.runtime.panicTypeError(fmt.Sprintf("JSON.stringify: invalid length: %v (%[1]T)", value)))
|
|
}
|
|
array := make([]interface{}, length)
|
|
for index := range array {
|
|
name := arrayIndexToString(int64(index))
|
|
value, _ := builtinJSONStringifyWalk(ctx, name, holder)
|
|
array[index] = value
|
|
}
|
|
return array, true
|
|
} else if holder.class != classFunctionName {
|
|
obj := map[string]interface{}{}
|
|
if ctx.propertyList != nil {
|
|
for _, name := range ctx.propertyList {
|
|
value, exists := builtinJSONStringifyWalk(ctx, name, holder)
|
|
if exists {
|
|
obj[name] = value
|
|
}
|
|
}
|
|
} else {
|
|
// Go maps are without order, so this doesn't conform to the ECMA ordering
|
|
// standard, but oh well...
|
|
holder.enumerate(false, func(name string) bool {
|
|
value, exists := builtinJSONStringifyWalk(ctx, name, holder)
|
|
if exists {
|
|
obj[name] = value
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
return obj, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|