function support added (off by default).

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2025-12-31 17:32:30 +03:00
parent 9fa5fcbc9f
commit 1543469cdf
2 changed files with 109 additions and 60 deletions

View File

@ -17,6 +17,8 @@ var NEG_INFINITY = '-Infinity'
var RECURSIVE = '<RECURSIVE%>' var RECURSIVE = '<RECURSIVE%>'
var FUNCTION = '<FUNCTION[%]>'
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -48,7 +50,7 @@ var debug = {
// string - string to use for indenting // string - string to use for indenting
// //
// //
// serialize(obj, base_path, seen, indent, depth) // _serialize(obj, base_path, seen, indent, depth, functions)
// -> str // -> str
// //
// //
@ -96,38 +98,31 @@ var debug = {
// ] // ]
// //
// //
// XXX add function support...
// XXX need to destinguish between map key and value in path...
// XXX BUG: using non-whitespace as indent breaks the depth of the first // XXX BUG: using non-whitespace as indent breaks the depth of the first
// or last elements in sequences // or last elements in sequences
// ...breaks .trim*() in Map/Set/Object... // ...breaks .trim*() in Map/Set/Object...
var serialize = var _serialize =
module.serialize = module._serialize =
function(obj, path=[], seen=new Map(), indent, depth=0){ function(obj, path=[], seen=new Map(), indent, depth=0, functions){
// args...
var args = [...arguments].slice(1)
if(typeof(args[0]) == 'number'
|| typeof(args[0]) == 'string'){
indent = args.shift() }
indent =
typeof(indent) == 'number' ?
' '.repeat(indent)
: indent
if(typeof(args[0]) == 'number'){
depth = args.shift() }
;[path, seen] = args
path ??= []
seen ??= new Map()
// recursive... // recursive...
var p = seen.get(obj) var p = seen.get(obj)
if(p != null){ if(p != null){
// NOTE: serialize(..) is always printed flat here, regardless of indent/depth... // NOTE: _serialize(..) is always printed flat here, regardless of indent/depth...
return RECURSIVE.replace('%', serialize(p)) } return RECURSIVE.replace('%', _serialize(p)) }
// XXX functions... // functions...
// XXX // NOTE: we are storing function length to avoid parsing the function...
// NOTE: storing length is a potential attack vector...
if(typeof(obj) == 'function'){
seen.set(obj, path)
// if functions array is given add function to it and store its
// index in the serialized data...
if(functions != null){
functions.push(obj)
obj = functions.length-1 }
var s = '('+ obj.toString() +')'
return FUNCTION
.replace('%', s.length +','+ s) }
// atomics... // atomics...
if(obj === null){ if(obj === null){
@ -157,20 +152,20 @@ function(obj, path=[], seen=new Map(), indent, depth=0){
for(var i=0; i < obj.length; i++){ for(var i=0; i < obj.length; i++){
elems.push( elems.push(
i in obj ? i in obj ?
serialize(obj[i], [...path, i], seen, indent, depth+1) _serialize(obj[i], [...path, i], seen, indent, depth+1, functions)
: EMPTY) } : EMPTY) }
} else if(obj instanceof Map){ } else if(obj instanceof Map){
pre = 'Map([' pre = 'Map(['
post = '])' post = '])'
elems = [ elems = [
serialize([...obj], path, seen, indent, depth) _serialize([...obj], path, seen, indent, depth, functions)
.slice(1, -1) .slice(1, -1)
.trim() ] .trim() ]
} else if(obj instanceof Set){ } else if(obj instanceof Set){
pre = 'Set([' pre = 'Set(['
post = '])' post = '])'
elems = [ elems = [
serialize([...obj], path, seen, indent, depth) _serialize([...obj], path, seen, indent, depth, functions)
.slice(1, -1) .slice(1, -1)
.trim() ] .trim() ]
} else { } else {
@ -180,7 +175,7 @@ function(obj, path=[], seen=new Map(), indent, depth=0){
elems.push(`${ elems.push(`${
JSON.stringify(k) JSON.stringify(k)
}:${ indent != null ? ' ' : '' }${ }:${ indent != null ? ' ' : '' }${
serialize(v, [...path, k], seen, indent, depth+1) _serialize(v, [...path, k], seen, indent, depth+1, functions)
// relevant for pretty-printing only... // relevant for pretty-printing only...
.trimLeft() .trimLeft()
}`) } } }`) } }
@ -198,6 +193,11 @@ function(obj, path=[], seen=new Map(), indent, depth=0){
return pre+ elems.join(join) +post } return pre+ elems.join(join) +post }
// user interface...
var serialize =
module.serialize =
function(obj, indent, depth=0, functions){
return _serialize(obj, [], new Map(), indent, depth, functions) }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -234,6 +234,8 @@ module.eJSON = {
'<empty>': 'empty', '<empty>': 'empty',
'<RECURSIVE': 'recursive', '<RECURSIVE': 'recursive',
'<FUNCTION[': 'func',
}, },
@ -305,7 +307,7 @@ module.eJSON = {
var k = path.at(-1) var k = path.at(-1)
if(parent instanceof Set){ if(parent instanceof Set){
var elems = [...parent] var elems = [...parent]
elems[k] = obj elems[k] = value
// we cant to keep the order... // we cant to keep the order...
parent.clear() parent.clear()
for(var e of elems){ for(var e of elems){
@ -543,9 +545,36 @@ module.eJSON = {
rec.push([path, obj]) rec.push([path, obj])
return [{}, i, line] }) }, return [{}, i, line] }) },
// XXX disable this by default...
// XXX can we avoid eval here???
func: function(state, path, match, str, i, line){ func: function(state, path, match, str, i, line){
// XXX if(state.functions == null){
}, this.error('Deserializing functions disabled.', str, i, line) }
debug.lex('function', str, i, line)
var res
// length...
i += match.length
var [l, i, line] = this.number(state, path, str[i], str, i, line)
if(str[i] != ','){
this.error('Expected "," got "'+ str[i] +'"', str, i, line) }
i++
// func ref...
if(state.functions instanceof Array){
var [n, i, line] = this.number(state, path, str[i+1], str, i+1, line)
res = state.functions[n]
// func code...
} else {
var code = str.slice(i, i+l)
i += l
line += code.split(/\n/g).length
if(str.slice(i, i+2) == ']>'){
res = eval?.(code) } }
if(str.slice(i, i+2) != ']>'){
this.error('Expected "]>" got "'+ str.slice(i, i+2) +'"', str, i, line) }
return [res, i+2, line] },
value: function(state, path, str, i=0, line=0){ value: function(state, path, str, i=0, line=0){
//debug.lex('value', str, i, line) //debug.lex('value', str, i, line)
@ -569,7 +598,7 @@ module.eJSON = {
var context = 8 var context = 8
var pre = Math.max(i-context, 0) var pre = Math.max(i-context, 0)
var post = Math.min(i+context, str.length) var post = Math.min(i+context, str.length)
this.error(`Unexpected: ${ str[i] }`, str, i, line) } this.error(`Unexpected: ${ str.slice(i, i+10) }`, str, i, line) }
// value... // value...
if(typeof(handler) != 'string'){ if(typeof(handler) != 'string'){
@ -578,10 +607,10 @@ module.eJSON = {
return this[handler](state, path, match, str, i, line) }, return this[handler](state, path, match, str, i, line) },
parse: function(str){ parse: function(str, functions){
// stage 1: build the object... // stage 1: build the object...
var state = {} var state = {functions}
var res = this.value(state, [], str)[0] var res = this.value(state, [], str)[0]
// stage 2: link the recursive structures... // stage 2: link the recursive structures...
@ -594,14 +623,13 @@ module.eJSON = {
var deserialize = var deserialize =
module.deserialize = module.deserialize =
function(str){ function(str, functions){
return eJSON.parse(str) } return eJSON.parse(str, functions) }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// utils... // utils...
// XXX see ./serialize.js
var deepCopy = var deepCopy =
module.deepCopy = module.deepCopy =
@ -610,6 +638,14 @@ function(obj){
serialize(obj)) } serialize(obj)) }
var semiDeepCopy =
module.semiDeepCopy =
function(obj){
var funcs = []
return deserialize(
serialize(obj, null, 0, funcs),
funcs) }
/********************************************************************** /**********************************************************************

55
test.js
View File

@ -28,6 +28,17 @@ var ejson = false
// XXX test whitespace handling... // XXX test whitespace handling...
var setups = test.Setups({ var setups = test.Setups({
'true': function(assert){
return ['true', json] },
'false': function(assert){
return ['false', json] },
'null': function(assert){
return ['null', json] },
'undefined': function(assert){
return ['undefined'] },
'NaN': function(assert){
return ['NaN'] },
number: function(assert){ number: function(assert){
return ['123', json] }, return ['123', json] },
'number-neg': function(assert){ 'number-neg': function(assert){
@ -49,20 +60,10 @@ var setups = test.Setups({
return ['Infinity'] }, return ['Infinity'] },
nInfinity: function(assert){ nInfinity: function(assert){
return ['-Infinity'] }, return ['-Infinity'] },
// XXX also test diffrerent quotations... // XXX also test diffrerent quotations...
string: function(assert){ string: function(assert){
return ['"string"', json] }, return ['"string"', json] },
'true': function(assert){
return ['true', json] },
'false': function(assert){
return ['false', json] },
'null': function(assert){
return ['null', json] },
'undefined': function(assert){
return ['undefined'] },
'NaN': function(assert){
return ['NaN'] },
'array-empty': function(assert){ 'array-empty': function(assert){
return ['[]', json] }, return ['[]', json] },
@ -74,8 +75,6 @@ var setups = test.Setups({
return ['[<empty>,"a"]'] }, return ['[<empty>,"a"]'] },
'array-sparse-d': function(assert){ 'array-sparse-d': function(assert){
return ['[<empty>,1,<empty>,<empty>,2,<empty>]'] }, return ['[<empty>,1,<empty>,<empty>,2,<empty>]'] },
'array-recursive': function(assert){
return ['[<RECURSIVE[]>]'] },
'object-empty': function(assert){ 'object-empty': function(assert){
return ['{}', json] }, return ['{}', json] },
@ -86,7 +85,21 @@ var setups = test.Setups({
'map-empty': function(assert){ 'map-empty': function(assert){
return ['Map([])'] }, return ['Map([])'] },
// XXX 'function': function(assert){
return ['<FUNCTION[14,(function(){})]>'] },
// recursive...
'array-recursive': function(assert){
return ['[<RECURSIVE[]>]'] },
'object-recursive': function(assert){
return ['{"r":<RECURSIVE[]>}'] },
'set-recursive': function(assert){
return ['Set([<RECURSIVE[]>])'] },
'map-recursive-key': function(assert){
return ['Map([[<RECURSIVE[]>,"value"]])'] },
'map-recursive-value': function(assert){
return ['Map([["key",<RECURSIVE[]>]])'] },
}) })
test.Modifiers({ test.Modifiers({
@ -95,32 +108,32 @@ test.Modifiers({
'array-stuffed': function(assert, [setup, json]){ 'array-stuffed': function(assert, [setup, json]){
return [ return [
eJSON.serialize( eJSON.serialize(
[ eJSON.deserialize(setup) ] ), [ eJSON.deserialize(setup, true) ] ),
json, json,
] }, ] },
'object-stuffed': function(assert, [setup, json]){ 'object-stuffed': function(assert, [setup, json]){
return [ return [
eJSON.serialize( eJSON.serialize(
{ key: eJSON.deserialize(setup) } ), { key: eJSON.deserialize(setup, true) } ),
json, json,
] }, ] },
// NOTE: these explicitly remove JSON compatibility regardless of input... // NOTE: these explicitly remove JSON compatibility regardless of input...
'set-stuffed': function(assert, [setup]){ 'set-stuffed': function(assert, [setup]){
return [ return [
eJSON.serialize( eJSON.serialize(
new Set([ eJSON.deserialize(setup) ]) ), new Set([ eJSON.deserialize(setup, true) ]) ),
ejson, ejson,
] }, ] },
'map-key-stuffed': function(assert, [setup]){ 'map-key-stuffed': function(assert, [setup]){
return [ return [
eJSON.serialize( eJSON.serialize(
new Map([[eJSON.deserialize(setup), "value"]]) ), new Map([[eJSON.deserialize(setup, true), "value"]]) ),
ejson, ejson,
] }, ] },
'map-value-stuffed': function(assert, [setup]){ 'map-value-stuffed': function(assert, [setup]){
return [ return [
eJSON.serialize( eJSON.serialize(
new Map([["key", eJSON.deserialize(setup)]]) ), new Map([["key", eJSON.deserialize(setup, true)]]) ),
ejson, ejson,
] }, ] },
}) })
@ -129,14 +142,14 @@ test.Tests({
'self-apply': function(assert, [setup]){ 'self-apply': function(assert, [setup]){
var res var res
assert( assert(
setup == (res = eJSON.serialize( eJSON.deserialize(setup) ) ), setup == (res = eJSON.serialize( eJSON.deserialize(setup, true) ) ),
'serialize(deserialize( setup )) == setup: expected:', setup, 'got:', res) }, 'serialize(deserialize( setup )) == setup: expected:', setup, 'got:', res) },
'json-caopatiility': function(assert, [setup, json], skipTest){ 'json-caopatiility': function(assert, [setup, json], skipTest){
if(!json){ if(!json){
skipTest() skipTest()
return } return }
var obj = eJSON.deserialize(setup) var obj = eJSON.deserialize(setup, true)
assert( assert(
JSON.stringify(obj) == eJSON.serialize(obj) ) }, JSON.stringify(obj) == eJSON.serialize(obj) ) },
}) })