1
0
mirror of https://github.com/robertkrimen/otto synced 2025-10-12 20:27:30 +08:00
This closes #37, #11
This commit is contained in:
Tim Jurcka 2013-07-15 17:25:47 +02:00 committed by Robert Krimen
parent 329e5afb2c
commit 5fe23327c9
9 changed files with 575 additions and 6 deletions

276
builtin_json.go Normal file
View File

@ -0,0 +1,276 @@
package otto
import (
"bytes"
"encoding/json"
"math"
"strings"
)
type _builtinJSON_parseContext struct {
call FunctionCall
reviver Value
}
func builtinJSON_parse(call FunctionCall) Value {
ctx := _builtinJSON_parseContext{
call: call,
}
revive := false
if reviver := call.Argument(1); reviver.isCallable() {
revive = true
ctx.reviver = reviver
}
var root interface{}
err := json.Unmarshal([]byte(toString(call.Argument(0))), &root)
if err != nil {
panic(newSyntaxError(err.Error()))
}
value, exists := builtinJSON_parseWalk(ctx, root)
if !exists {
value = UndefinedValue()
}
if revive {
root := ctx.call.runtime.newObject()
root.put("", value, false)
return builtinJSON_reviveWalk(ctx, root, "")
}
return value
}
func builtinJSON_reviveWalk(ctx _builtinJSON_parseContext, holder *_object, name string) Value {
value := holder.get(name)
if object := value._object(); object != nil {
if isArray(object) {
length := int64(objectLength(object))
for index := int64(0); index < length; index += 1 {
name := arrayIndexToString(index)
value := builtinJSON_reviveWalk(ctx, object, name)
if value.IsUndefined() {
object.delete(name, false)
} else {
object.defineProperty(name, value, 0111, false)
}
}
} else {
object.enumerate(false, func(name string) bool {
value := builtinJSON_reviveWalk(ctx, object, name)
if value.IsUndefined() {
object.delete(name, false)
} else {
object.defineProperty(name, value, 0111, false)
}
return true
})
}
}
return ctx.reviver.call(toValue_object(holder), name, value)
}
func builtinJSON_parseWalk(ctx _builtinJSON_parseContext, rawValue interface{}) (Value, bool) {
switch value := rawValue.(type) {
case nil:
return NullValue(), true
case bool:
return toValue_bool(value), true
case string:
return toValue_string(value), true
case float64:
return toValue_float64(value), true
case []interface{}:
arrayValue := make([]Value, len(value))
for index, rawValue := range value {
if value, exists := builtinJSON_parseWalk(ctx, rawValue); exists {
arrayValue[index] = value
}
}
return toValue_object(ctx.call.runtime.newArrayOf(arrayValue)), true
case map[string]interface{}:
object := ctx.call.runtime.newObject()
for name, rawValue := range value {
if value, exists := builtinJSON_parseWalk(ctx, rawValue); exists {
object.put(name, value, false)
}
}
return toValue_object(object), true
}
return Value{}, false
}
type _builtinJSON_stringifyContext struct {
call FunctionCall
stack []*_object
propertyList []string
replacerFunction *Value
gap string
}
func builtinJSON_stringify(call FunctionCall) Value {
ctx := _builtinJSON_stringifyContext{
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._valueType {
case valueObject:
switch value.value.(*_object).class {
case "String":
case "Number":
default:
continue
}
case valueString:
case valueNumber:
default:
continue
}
name := toString(value)
if seen[name] {
continue
}
seen[name] = true
length += 1
propertyList[index] = name
}
ctx.propertyList = propertyList[0:length]
} else if replacer.class == "Function" {
value := toValue_object(replacer)
ctx.replacerFunction = &value
}
}
if spaceValue, exists := call.getArgument(2); exists {
if spaceValue._valueType == valueObject {
switch spaceValue.value.(*_object).class {
case "String":
spaceValue = toValue_string(toString(spaceValue))
case "Number":
spaceValue = toNumber(spaceValue)
}
}
switch spaceValue._valueType {
case valueString:
value := toString(spaceValue)
if len(value) > 10 {
ctx.gap = value[0:10]
} else {
ctx.gap = value
}
case valueNumber:
value := toInteger(spaceValue).value
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 := builtinJSON_stringifyWalk(ctx, "", holder)
if !exists {
return UndefinedValue()
}
valueJSON, err := json.Marshal(value)
if err != nil {
panic(newTypeError(err.Error()))
}
if ctx.gap != "" {
valueJSON1 := bytes.Buffer{}
json.Indent(&valueJSON1, valueJSON, "", ctx.gap)
valueJSON = valueJSON1.Bytes()
}
return toValue_string(string(valueJSON))
}
func builtinJSON_stringifyWalk(ctx _builtinJSON_stringifyContext, key string, holder *_object) (interface{}, bool) {
value := holder.get(key)
if value.IsObject() {
if toJSON := value._object().get("toJSON"); toJSON.IsFunction() {
value = toJSON.call(value, key)
}
}
if ctx.replacerFunction != nil {
value = (*ctx.replacerFunction).call(toValue_object(holder), key, value)
}
if value._valueType == valueObject {
switch value.value.(*_object).class {
case "Boolean":
value = value._object().value.(Value)
case "String":
value = toValue_string(toString(value))
case "Number":
value = toNumber(value)
}
}
switch value._valueType {
case valueBoolean:
return toBoolean(value), true
case valueString:
return toString(value), true
case valueNumber:
value := toFloat(value)
if math.IsNaN(value) || math.IsInf(value, 0) {
return nil, true
}
return value, true
case valueNull:
return nil, true
case valueObject:
holder := value._object()
if value := value._object(); nil != value {
for _, object := range ctx.stack {
if holder == object {
panic(newTypeError("Converting circular structure to JSON"))
}
}
ctx.stack = append(ctx.stack, value)
defer func() { ctx.stack = ctx.stack[:len(ctx.stack)-1] }()
}
if isArray(holder) {
length := holder.get("length").value.(uint32)
array := make([]interface{}, length)
for index, _ := range array {
name := arrayIndexToString(int64(index))
value, _ := builtinJSON_stringifyWalk(ctx, name, holder)
array[index] = value
}
return array, true
} else if holder.class != "Function" {
object := map[string]interface{}{}
if ctx.propertyList != nil {
for _, name := range ctx.propertyList {
value, exists := builtinJSON_stringifyWalk(ctx, name, holder)
if exists {
object[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 := builtinJSON_stringifyWalk(ctx, name, holder)
if exists {
object[name] = value
}
return true
})
}
return object, true
}
}
return nil, false
}

View File

@ -43,6 +43,7 @@ func (runtime *_runtime) clone() *_runtime {
clone.object(runtime.Global.ReferenceError),
clone.object(runtime.Global.SyntaxError),
clone.object(runtime.Global.URIError),
clone.object(runtime.Global.JSON),
clone.object(runtime.Global.ObjectPrototype),
clone.object(runtime.Global.FunctionPrototype),

View File

@ -62,7 +62,7 @@ func TestGlobal(t *testing.T) {
test(`
Object.getOwnPropertyNames(Function('return this')()).sort();
`, "Array,Boolean,Date,Error,EvalError,Function,Infinity,Math,NaN,Number,Object,RangeError,ReferenceError,RegExp,String,SyntaxError,TypeError,URIError,console,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,escape,eval,isFinite,isNaN,parseFloat,parseInt,undefined,unescape")
`, "Array,Boolean,Date,Error,EvalError,Function,Infinity,JSON,Math,NaN,Number,Object,RangeError,ReferenceError,RegExp,String,SyntaxError,TypeError,URIError,console,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,escape,eval,isFinite,isNaN,parseFloat,parseInt,undefined,unescape")
// __defineGetter__,__defineSetter__,__lookupGetter__,__lookupSetter__,constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf
test(`

16
inline
View File

@ -582,6 +582,21 @@ sub newContext {
});
} qw/Eval Type Range Reference Syntax URI/),
# JSON
$self->block(sub {
my $class = "JSON";
return
".$class =",
$self->globalObject(
$class,
$self->functionDeclare(
$class,
"parse", 2,
"stringify", 3,
),
),
}),
# Global
$self->block(sub {
my $class = "Global";
@ -618,6 +633,7 @@ sub newContext {
"ReferenceError",
"SyntaxError",
"URIError",
"JSON",
),
$self->property("undefined", $self->undefinedValue(), "0"),
$self->property("NaN", $self->numberValue("math.NaN()"), "0"),

View File

@ -5464,6 +5464,79 @@ func _newContext(runtime *_runtime) {
},
}
}
{
parse_function := &_object{
runtime: runtime,
class: "Function",
objectClass: _classObject,
prototype: runtime.Global.FunctionPrototype,
extensible: true,
property: map[string]_property{
"length": _property{
mode: 0,
value: Value{
_valueType: valueNumber,
value: 2,
},
},
},
propertyOrder: []string{
"length",
},
value: _functionObject{
call: _nativeCallFunction(builtinJSON_parse),
},
}
stringify_function := &_object{
runtime: runtime,
class: "Function",
objectClass: _classObject,
prototype: runtime.Global.FunctionPrototype,
extensible: true,
property: map[string]_property{
"length": _property{
mode: 0,
value: Value{
_valueType: valueNumber,
value: 3,
},
},
},
propertyOrder: []string{
"length",
},
value: _functionObject{
call: _nativeCallFunction(builtinJSON_stringify),
},
}
runtime.Global.JSON = &_object{
runtime: runtime,
class: "JSON",
objectClass: _classObject,
prototype: runtime.Global.ObjectPrototype,
extensible: true,
property: map[string]_property{
"parse": _property{
mode: 0101,
value: Value{
_valueType: valueObject,
value: parse_function,
},
},
"stringify": _property{
mode: 0101,
value: Value{
_valueType: valueObject,
value: stringify_function,
},
},
},
propertyOrder: []string{
"parse",
"stringify",
},
}
}
{
eval_function := &_object{
runtime: runtime,
@ -5897,6 +5970,13 @@ func _newContext(runtime *_runtime) {
value: runtime.Global.URIError,
},
},
"JSON": _property{
mode: 0101,
value: Value{
_valueType: valueObject,
value: runtime.Global.JSON,
},
},
"undefined": _property{
mode: 0,
value: Value{
@ -5946,6 +6026,7 @@ func _newContext(runtime *_runtime) {
"ReferenceError",
"SyntaxError",
"URIError",
"JSON",
"undefined",
"NaN",
"Infinity",

185
json_test.go Normal file
View File

@ -0,0 +1,185 @@
package otto
import (
. "./terst"
"testing"
"time"
)
func BenchmarkJSON_parse(b *testing.B) {
otto := New()
for i := 0; i < b.N; i++ {
otto.Run(`JSON.parse("1")`)
otto.Run(`JSON.parse("[1,2,3]")`)
otto.Run(`JSON.parse('{"a":{"x":100,"y":110},"b":[10,20,30],"c":"zazazaza"}')`)
otto.Run(`JSON.parse("[1,2,3]", function(k, v) { return undefined })`)
}
}
func TestJSON_parse(t *testing.T) {
Terst(t)
test := runTest()
test(`
JSON.parse("1");
`, "1")
test(`
JSON.parse("null");
`, "null")
test(`
var abc = JSON.parse('"a\uFFFFbc"');
[ abc[0], abc[2], abc[3], abc.length ];
`, "a,b,c,4")
test(`
JSON.parse("[1, 2, 3]");
`, "1,2,3")
test(`
JSON.parse('{ "abc": 1, "def":2 }').abc;
`, "1")
test(`
JSON.parse('{ "abc": { "x": 100, "y": 110 }, "def": [ 10, 20 ,30 ], "ghi": "zazazaza" }').def;
`, "10,20,30")
test(`raise:
JSON.parse("12\t\r\n 34");
`, "SyntaxError: invalid character '3' after top-level value")
test(`
JSON.parse("[1, 2, 3]", function() { return undefined });
`, "undefined")
test(`raise:
JSON.parse("");
`, "SyntaxError: unexpected end of JSON input")
test(`raise:
JSON.parse("[1, 2, 3");
`, "SyntaxError: unexpected end of JSON input")
test(`raise:
JSON.parse("[1, 2, ; abc=10");
`, "SyntaxError: invalid character ';' looking for beginning of value")
test(`raise:
JSON.parse("[1, 2, function(){}]");
`, "SyntaxError: invalid character 'u' in literal false (expecting 'a')")
}
func TestJSON_stringify(t *testing.T) {
Terst(t)
test := runTest()
defer mockTimeLocal(time.UTC)()
test(`
JSON.stringify(function(){});
`, "undefined")
test(`
JSON.stringify(new Boolean(false));
`, "false")
test(`
JSON.stringify({a1: {b1: [1,2,3,4], b2: {c1: 1, c2: 2}}, a2: 'a2'}, null, -5);
`, `{"a1":{"b1":[1,2,3,4],"b2":{"c1":1,"c2":2}},"a2":"a2"}`)
test(`
JSON.stringify(undefined);
`, "undefined")
test(`
JSON.stringify(1);
`, "1")
test(`
JSON.stringify("abc def");
`, "\"abc def\"")
test(`
JSON.stringify(3.14159);
`, "3.14159")
test(`
JSON.stringify([]);
`, "[]")
test(`
JSON.stringify([1, 2, 3]);
`, "[1,2,3]")
test(`
JSON.stringify([true, false, null]);
`, "[true,false,null]")
test(`
JSON.stringify({
abc: { x: 100, y: 110 },
def: [ 10, 20, 30 ],
ghi: "zazazaza"
});
`, `{"abc":{"x":100,"y":110},"def":[10,20,30],"ghi":"zazazaza"}`)
test(`
JSON.stringify([
'e',
{pluribus: 'unum'}
], null, '\t');
`, "[\n\t\"e\",\n\t{\n\t\t\"pluribus\": \"unum\"\n\t}\n]")
test(`
JSON.stringify(new Date(0));
`, `"1970-01-01T00:00:00.000Z"`)
test(`
JSON.stringify([ new Date(0) ], function(key, value){
return this[key] instanceof Date ? 'Date(' + this[key] + ')' : value
});
`, `["Date(Thu, 01 Jan 1970 00:00:00 UTC)"]`)
test(`
JSON.stringify({
abc: 1,
def: 2,
ghi: 3
}, ['abc','def']);
`, `{"abc":1,"def":2}`)
test(`raise:
var abc = {
def: null
};
abc.def = abc;
JSON.stringify(abc)
`, "TypeError: Converting circular structure to JSON")
test(`raise:
var abc= [ null ];
abc[0] = abc;
JSON.stringify(abc);
`, "TypeError: Converting circular structure to JSON")
test(`raise:
var abc = {
def: {}
};
abc.def.ghi = abc;
JSON.stringify(abc)
`, "TypeError: Converting circular structure to JSON")
test(`
var ghi = { "pi": 3.14159 };
var abc = {
def: {}
};
abc.ghi = ghi;
abc.def.ghi = ghi;
JSON.stringify(abc);
`, `{"def":{"ghi":{"pi":3.14159}},"ghi":{"pi":3.14159}}`)
}

View File

@ -55,14 +55,19 @@ func arrayIndexToString(index int64) string {
return strconv.FormatInt(index, 10)
}
func valueOfArrayIndex(list []Value, index int) Value {
if index >= 0 && index < len(list) {
value := list[index]
func valueOfArrayIndex(array []Value, index int) Value {
value, _ := getValueOfArrayIndex(array, index)
return value
}
func getValueOfArrayIndex(array []Value, index int) (Value, bool) {
if index >= 0 && index < len(array) {
value := array[index]
if !value.isEmpty() {
return value
return value, true
}
}
return UndefinedValue()
return UndefinedValue(), false
}
// A range index can be anything from 0 up to length. It is NOT safe to use as an index

View File

@ -22,6 +22,7 @@ type _global struct {
ReferenceError *_object
SyntaxError *_object
URIError *_object
JSON *_object
ObjectPrototype *_object // Object.prototype
FunctionPrototype *_object // Function.prototype

View File

@ -276,6 +276,10 @@ func (self FunctionCall) Argument(index int) Value {
return valueOfArrayIndex(self.ArgumentList, index)
}
func (self FunctionCall) getArgument(index int) (Value, bool) {
return getValueOfArrayIndex(self.ArgumentList, index)
}
func (self FunctionCall) slice(index int) []Value {
if index < len(self.ArgumentList) {
return self.ArgumentList[index:]