From 4c17b6ca3c5edd1b4b7e747a7d7c448c5b4d2873 Mon Sep 17 00:00:00 2001 From: "Alex A. Naanou" Date: Thu, 1 Jan 2026 18:44:38 +0300 Subject: [PATCH] added bigint support... Signed-off-by: Alex A. Naanou --- package.json | 2 +- serialize.js | 73 ++++-- serialize2.js | 645 -------------------------------------------------- test.js | 2 + 4 files changed, 53 insertions(+), 669 deletions(-) delete mode 100644 serialize2.js diff --git a/package.json b/package.json index 83550ac..10794db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ig-serialize", - "version": "1.0.1", + "version": "1.0.2", "description": "experimental extended json serializaion...", "main": "serialize.js", "scripts": { diff --git a/serialize.js b/serialize.js index 63faf14..8b22b24 100644 --- a/serialize.js +++ b/serialize.js @@ -41,8 +41,11 @@ var debug = { //--------------------------------------------------------------------- +module.STRING_LENGTH_REF = 64 + // -// serialize(obj[, indent[, depth]]) +// serialize(obj[, options]) +// serialize(obj[, indent[, depth[, options]]]) // -> str // // indent can be: @@ -50,7 +53,7 @@ var debug = { // string - string to use for indenting // // -// _serialize(obj, base_path, seen, indent, depth, functions) +// _serialize(obj, base_path, seen, indent, depth, options) // -> str // // @@ -103,7 +106,11 @@ var debug = { // ...breaks .trim*() in Map/Set/Object... var _serialize = module._serialize = -function(obj, path=[], seen=new Map(), indent, depth=0, functions){ +function(obj, path=[], seen=new Map(), indent, depth=0, options={}){ + var string_length_ref = + options.string_length_ref + ?? module.STRING_LENGTH_REF + // recursive... var p = seen.get(obj) if(p != null){ @@ -117,14 +124,24 @@ function(obj, path=[], seen=new Map(), indent, depth=0, functions){ seen.set(obj, path) // if functions array is given add function to it and store its // index in the serialized data... - if(functions instanceof Array){ - functions.push(obj) - obj = functions.length-1 } + if(options.functions instanceof Array){ + options.functions.push(obj) + obj = options.functions.length-1 } var s = '('+ obj.toString() +')' return FUNCTION .replace('%', s.length +','+ s) } + // special case: long strings... + if(typeof(obj) == 'string' + && obj.length > string_length_ref){ + seen.set(obj, path) } + // BigInt... + if(typeof(obj) == 'bigint'){ + seen.set(obj, path) + return obj.toString() +'n' } + // atomics... + // NOTE: these are not stored in seen thus are not re-referenced... if(obj === null){ return NULL } if(typeof(obj) != 'object'){ @@ -137,6 +154,8 @@ function(obj, path=[], seen=new Map(), indent, depth=0, functions){ INFINITY : obj === -Infinity ? NEG_INFINITY + // XXX might be a good idea to reference really long strings instead + // of storing each... : JSON.stringify(obj, null, indent) } // objects... @@ -152,20 +171,20 @@ function(obj, path=[], seen=new Map(), indent, depth=0, functions){ for(var i=0; i < obj.length; i++){ elems.push( i in obj ? - _serialize(obj[i], [...path, i], seen, indent, depth+1, functions) + _serialize(obj[i], [...path, i], seen, indent, depth+1, options) : EMPTY) } } else if(obj instanceof Map){ pre = 'Map([' post = '])' elems = [ - _serialize([...obj], path, seen, indent, depth, functions) + _serialize([...obj], path, seen, indent, depth, options) .slice(1, -1) .trim() ] } else if(obj instanceof Set){ pre = 'Set([' post = '])' elems = [ - _serialize([...obj], path, seen, indent, depth, functions) + _serialize([...obj], path, seen, indent, depth, options) .slice(1, -1) .trim() ] } else { @@ -175,7 +194,7 @@ function(obj, path=[], seen=new Map(), indent, depth=0, functions){ elems.push(`${ JSON.stringify(k) }:${ indent != null ? ' ' : '' }${ - _serialize(v, [...path, k], seen, indent, depth+1, functions) + _serialize(v, [...path, k], seen, indent, depth+1, options) // relevant for pretty-printing only... .trimLeft() }`) } } @@ -196,8 +215,8 @@ function(obj, path=[], seen=new Map(), indent, depth=0, functions){ // user interface... var serialize = module.serialize = -function(obj, indent, depth=0, functions){ - return _serialize(obj, [], new Map(), indent, depth, functions) } +function(obj, indent, depth=0, options){ + return _serialize(obj, [], new Map(), indent, depth, options) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -357,6 +376,9 @@ module.eJSON = { || (str[j] >= '0' && str[j] <= '9'))){ j++ } + // BigInt... + if(str[j] == 'n'){ + return [ BigInt(str.slice(i, j)), j+1, line ] } return [ str.slice(i, j)*1, j, line ] }, // XXX TEST count \\n string: function(state, path, match, str, i, line){ @@ -543,7 +565,6 @@ module.eJSON = { // NOTE: this uses eval(..) so care must be taken when enabling this... func: function(state, path, match, str, i, line){ if(state.functions == null){ - console.log('---', state) this.error('Deserializing functions disabled.', str, i, line) } debug.lex('function', str, i, line) @@ -555,7 +576,7 @@ module.eJSON = { this.error('Expected "," got "'+ str[i] +'"', str, i, line) } i++ - // func ref... + // func index... if(state.functions instanceof Array){ var [n, i, line] = this.number(state, path, str[i+1], str, i+1, line) res = state.functions[n] @@ -604,10 +625,10 @@ module.eJSON = { return this[handler](state, path, match, str, i, line) }, - parse: function(str, functions){ + parse: function(str, options={}){ // stage 1: build the object... - var state = {functions} + var state = {functions: options.functions} var res = this.value(state, [], str)[0] // stage 2: link the recursive structures... @@ -620,8 +641,12 @@ module.eJSON = { var deserialize = module.deserialize = -function(str, functions){ - return eJSON.parse(str, functions) } +function(str, options){ + options = + options === true ? + {functions: true} + : options + return eJSON.parse(str, options) } @@ -630,18 +655,20 @@ function(str, functions){ var deepCopy = module.deepCopy = -function(obj, functions){ +function(obj, funcs){ + var options = {functions: funcs} return deserialize( - serialize(obj, null, 0, functions), - functions) } + serialize(obj, null, 0, options), + options) } var partialDeepCopy = module.partialDeepCopy = function(obj, funcs=[]){ + var options = {functions: funcs} return deserialize( - serialize(obj, null, 0, funcs), - funcs) } + serialize(obj, null, 0, options), + options) } diff --git a/serialize2.js b/serialize2.js deleted file mode 100644 index e1f3a49..0000000 --- a/serialize2.js +++ /dev/null @@ -1,645 +0,0 @@ -/********************************************************************** -* -* -* -**********************************************************************/ -((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) -(function(require){ var module={} // make module AMD/node compatible... -/*********************************************************************/ - - -var EMPTY = '' -var NULL = 'null' -var UNDEFINED = 'undefined' -var NAN = 'NaN' -var INFINITY = 'Infinity' -var NEG_INFINITY = '-Infinity' - -var RECURSIVE = '' - -var FUNCTION = '' - - - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -module.DEBUG = false - -var DEBUG_PREFIX = '---' - -var debug = { - context: 16, - - log: function(...args){ - if(!module.DEBUG){ - return } - return console.log(DEBUG_PREFIX, ...arguments) }, - lex: function(name, str, i, line){ - return this.log(name +':', str.slice(i, i+this.context) +'...') }, -} - - -//--------------------------------------------------------------------- - -// -// serialize(obj[, indent[, depth]]) -// -> str -// -// indent can be: -// number - number of spaces to use for indent -// string - string to use for indenting -// -// -// _serialize(obj, base_path, seen, indent, depth, functions) -// -> str -// -// -// Paths -// A path is a chain of indexes/keys leading to a specific object in -// tree. -// -// the root object is referenced by an empty path array. -// -// For sets, positional indexes are used, i.e. a set is treated like -// an array ov values. -// -// For maps a two number index is used with the first being the position -// of the item and the second indicated if the target is the key or the -// value. i.e. a map is treated like an array of key value pairs. -// -// -// Examples: -// Object paths -// --------------------------------------------------------------- -// obj = [ <- [] -// null, <- [0] -// [ -// 'a', -// 'b', <- [1, 1] -// 'c', -// ], -// Set([ -// 1, -// 2, -// 3, <- [2, 2] -// ]), -// Map([i <- [3] +----- index of elemnt in map -// [ / +--- 0 means key -// 'key', <- [3, 0, 0] -// 'value' <- [3, 0, 1] -// ], +--- 1 means value -// [ -// [ -// 123 <- [3, 1, 0, 0] -// ], -// 'got tired of thinking up names', -// ], -// ]), -// ] -// -// -// XXX BUG: using non-whitespace as indent breaks the depth of the first -// or last elements in sequences -// ...breaks .trim*() in Map/Set/Object... -var _serialize = -module._serialize = -function(obj, path=[], seen=new Map(), indent, depth=0, functions){ - // recursive... - var p = seen.get(obj) - if(p != null){ - // NOTE: _serialize(..) is always printed flat here, regardless of indent/depth... - return RECURSIVE.replace('%', _serialize(p)) } - - // functions... - // 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... - if(obj === null){ - return NULL } - if(typeof(obj) != 'object'){ - return typeof(obj) == 'number' - && isNaN(obj) ? - NAN - : obj === undefined ? - UNDEFINED - : obj === Infinity ? - INFINITY - : obj === -Infinity ? - NEG_INFINITY - : JSON.stringify(obj, null, indent) } - - // objects... - seen.set(obj, path) - - var elems = [] - var pre = '' - var join = ',' - var post = '' - if(obj instanceof Array){ - pre = '[' - post = ']' - for(var i=0; i < obj.length; i++){ - elems.push( - i in obj ? - _serialize(obj[i], [...path, i], seen, indent, depth+1, functions) - : EMPTY) } - } else if(obj instanceof Map){ - pre = 'Map([' - post = '])' - elems = [ - _serialize([...obj], path, seen, indent, depth, functions) - .slice(1, -1) - .trim() ] - } else if(obj instanceof Set){ - pre = 'Set([' - post = '])' - elems = [ - _serialize([...obj], path, seen, indent, depth, functions) - .slice(1, -1) - .trim() ] - } else { - pre = '{' - post = '}' - for(var [k, v] of Object.entries(obj)){ - elems.push(`${ - JSON.stringify(k) - }:${ indent != null ? ' ' : '' }${ - _serialize(v, [...path, k], seen, indent, depth+1, functions) - // relevant for pretty-printing only... - .trimLeft() - }`) } } - - // handle indent... - if(indent != null){ - i = indent.repeat(depth) - s = i + indent - if(elems.length > 0){ - pre = pre + '\n' + s - post = '\n' + i + post - // XXX set limit for number of elements to keep horizontal... - // ...also account for element length... - join = join + '\n' + s } } - - 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) } - - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -// XXX better error handling... -// XXX try and make this single stage (see notes for : .recursive(..)) -var eJSON = -module.eJSON = { - chars: { - '"': 'string', "'": 'string', '`': 'string', - - 0: 'number', 1: 'number', 2: 'number', 3: 'number', 4: 'number', - 5: 'number', 6: 'number', 7: 'number', 8: 'number', 9: 'number', - '.': 'number', '-': 'number', - - '[': 'array', - '{': 'object', - }, - words: { - true: true, - false: false, - - Infinity: 'number', - - 'Set(': 'set', - 'Map(': 'map', - - null: null, - undefined: undefined, - NaN: NaN, - - '': 'empty', - ' 0 - && i - pre <= this.context_size - && str[pre] != '\n'){ - pre-- } - while(post < str.length - && post - i <= this.context_size - && str[post] != '\n'){ - post++ } - throw new SyntaxError(`${ msg }\n` - +` ${line}: ${ str.slice(pre, post) }\n` - +` ${ ' '.repeat( i - pre + ((line + ': ').length) ) }^`) }, - - // - // ._getItem(obj, path) - // -> [path, obj] - // - // NOTE: we are returning path here because we need to distinguish - // incomplete paths that we can infer the container from and - // complete paths, e.g: - // For - // m = [new Map([[1,2], [2,3]])] - // Both of the below will return the map and the correct path ([0]): - // eJSON._getItem(m, [0, 1]) - // eJSON._getItem(m, [0]) - // But we need to destinguish between the two cases when - // writing to a map (see: .setItem(..)) - _getItem: function(obj, path){ - for(var i=0; i obj - // - // NOTE: this is a POLS wrapper of ._getItem(..) - getItem: function(obj, path){ - return this._getItem(...arguments)[1] }, - // NOTE: this behaves in a similar way to normal key value assignment, - // i.e. replacing items if they exist, this is not a problem here - // as in the general case we are assigning over a placeholder - // object... - // NOTE: as a side-effect this maintains element oreder even in object - // with no direct concept of element order, like objects, sets and - // maps. - // NOTE: some operations envolve rewriting the container elements so - // are not as fast, namely writing set elements and map keys. - setItem: function(obj, path, value){ - var [p, parent] = this._getItem(obj, path.slice(0, -1)) - var k = path.at(-1) - if(parent instanceof Set){ - var elems = [...parent] - elems[k] = value - // we cant to keep the order... - parent.clear() - for(var e of elems){ - parent.add(e) } - } else if(parent instanceof Map){ - if(path.length-2 !== p.length){ - throw new Error('.setItem(..): incomplete path.') } - var i = path.at(-2) - // replace the index... - if(k == 0){ - var elems = [...parent] - elems[i][0] = value - parent.clear() - for(var e of elems){ - parent.set(...e) } - // set the value... - } else { - parent.set([...parent][i][0], value) } - } else { - parent[k] = value } - return value }, - - - - WHITESPACE: ' \t\n', - skipWhitespace: function(str, i, line){ - while(i < str.length - && this.WHITESPACE.includes(str[i])){ - if(str[i] == '\n'){ - line++ } - i++ } - return [i, line] }, - - // - // .handler(match, str, i, line) - // -> [value, i, line] - // - number: function(state, path, match, str, i, line){ - debug.lex('number', str, i, line) - // special cases.., - if(match == 'Infinity'){ - return [Infinity, i+'Infinity'.length, line] } - if(match == '-' - && str.slice(i, i+'-Infinity'.length) == '-Infinity'){ - return [-Infinity, i+'-Infinity'.length, line] } - // numbers... - var j = i+1 - while(j < str.length - && (str[j] == '.' - || (str[j] >= '0' - && str[j] <= '9'))){ - j++ } - return [ str.slice(i, j)*1, j, line ] }, - // XXX TEST count \\n - string: function(state, path, match, str, i, line){ - debug.lex('string', str, i, line) - var j = i+1 - while(j < str.length - && str[j] != match){ - // newlines... - if(str[j] == '\n'){ - line++ } - // escaped newlines... - if(str[j] == '\\' - && j+1 < str.length - && str[j+1] == 'n'){ - line++ } - // skip escaped quotes... - if(str[j] == '\\' - && j+1 < str.length - && str[j+1] == match){ - j++ } - j++ } - if(j == str.length - && str[j-1] != match){ - this.error('Unexpected end of input wile looking fot "'+ match +'".', str, i, line) } - return [ str.slice(i+1, j), j+1, line ] }, - identifier: function(state, path, match, str, i, line){ - debug.lex('identifier', str, i, line) - if(!/[a-zA-Z_]/.test(str[i])){ - this.error('Not an identifier: "'+ str[i] +'"', str, i, line) } - var j = i+1 - while(j < str.length - && /[a-zA-Z0-9_]/.test(str[j])){ - j++ } - return [ str.slice(i, j), j, line ] }, - - // - // handler(res, index, str, i, line) - // -> [res, i, line] - // - sequence: function(state, path, str, i, line, end, handler, initial=[]){ - var index = 0 - while(i < str.length){ - ;[i, line] = this.skipWhitespace(str, i, line) - - // done... - if(str.slice(i, i+end.length) == end){ - return [initial, i+end.length, line] } - - // empty... - if(str[i] == ','){ - index++ - i++ - // XXX this feels hackish -- can this be deligated to the handler??? - initial instanceof Array - && initial.length++ - continue } - if(str.slice(i, i+EMPTY.length) == EMPTY){ - index++ - i += EMPTY.length - if(str[i] == ','){ - i++ } - // XXX this feels hackish -- can this be deligated to the handler??? - initial instanceof Array - && initial.length++ - continue } - - // end of input... - if(i >= str.length-1){ - break } - - // value... - ;[initial, i, line] = handler.call(this, initial, index, str, i, line) - - ;[i, line] = this.skipWhitespace(str, i, line) - if(str[i] == ','){ - i++ } - index++ } - - // XXX better message -- show starting seq... - this.error('Unexpected end of input wile looking for "'+ end +'".', str, i, line) }, - - array: function(state, path, match, str, i, line){ - debug.lex('array', str, i, line) - return this.sequence( - state, path, str, i+1, line, - ']', - function(res, index, str, i, line){ - var obj - ;[obj, i, line] = this.value(state, [...path, index], str, i, line) - res[index] = obj - return [res, i, line] }) }, - object: function(state, path, match, str, i, line){ - debug.lex('object', str, i, line) - return this.sequence( - state, path, str, i+1, line, - '}', - function(res, index, str, i, line){ - var obj, key - // key... - ;[key, i, line] = '\'"`'.includes(str[i]) ? - this.string(state, path, str[i], str, i, line) - : this.identifier(state, path, str[i], str, i, line) - - // ':'... - ;[i, line] = this.skipWhitespace(str, i, line) - if(str[i] != ':'){ - this.error('Expected ":", got "'+ str[i] +'".', str, i, line) } - i++ - ;[i, line] = this.skipWhitespace(str, i, line) - - // value... - ;[obj, i, line] = this.value(state, [...path, key], str, i, line) - res[key] = obj - return [res, i, line] }, - {}) }, - - set: function(state, path, match, str, i, line){ - debug.lex('set', str, i, line) - i += match.length - ;[res, i, line] = this.sequence( - state, path, - str, i+1, line, - ']', - function(res, index, str, i, line){ - var obj - ;[obj, i, line] = this.value( - state, - [...path, index], - str, i, line) - res.add(obj) - return [res, i, line] }, - new Set()) - if(str[i] != ')'){ - this.error('Expected ")", got "'+ str[i] +'".', str, i, line) } - return [res, i+1, line] }, - map: function(state, path, match, str, i, line){ - debug.lex('map', str, i, line) - i += match.length - ;[res, i, line] = this.sequence( - state, path, - str, i+1, line, - ']', - function(res, index, str, i, line){ - debug.lex(' map-content', str, i, line) - var obj - ;[[key, value], i, line] = this.value( - state, - [...path, index], - str, i, line) - res.set(key, value) - return [res, i, line] }, - new Map()) - if(str[i] != ')'){ - this.error('Expected ")", got "'+ str[i] +'".', str, i, line) } - return [res, i+1, line] }, - - // XXX should this be done inline or on a separate stage? - // for inline we need these: - // - all containers must be placed as soon as they are created - // "return" before the items are processed... - // - containers must be "filled" as the items are parsed - // like .array(..) and .object(..) but not like .map(..) and .set(..) - // - thread the root object... - // alternatively, for a two stage process we need to: - // - have a mechanism to write placeholders - // - remember their positions (paths) - // - build/get said path - // XXX another strategy would be to have an path-object map and - // directly reference that -- this would eliminate the need for - // stage two... (XXX TEST) - // ...need to use serialized paths as keys... - recursive: function(state, path, match, str, i, line){ - debug.lex('recursive', str, i, line) - return this.sequence( - state, path, str, i+match.length, line, - '>', - function(res, index, str, i, line){ - var obj - ;[obj, i, line] = this.array(state, [...path, index], '[', str, i, line) - var rec = state.recursive ??= [] - rec.push([path, obj]) - return [{}, i, line] }) }, - - // NOTE: this uses eval(..) so care must be taken when enabling this... - func: function(state, path, match, str, i, line){ - 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){ - - ;[i, line] = this.skipWhitespace(str, i, line) - - // get handler... - var NONE = {} - var handler = NONE - var match = str[i] - if(match in this.chars){ - handler = this.chars[match] - } else { - for(match in this.words){ - if(match == str.slice(i, i+match.length)){ - handler = this.words[match] - break } } } - - // syntax error... - if(handler === NONE){ - var context = 8 - var pre = Math.max(i-context, 0) - var post = Math.min(i+context, str.length) - this.error(`Unexpected: ${ str.slice(i, i+10) }`, str, i, line) } - - // value... - if(typeof(handler) != 'string'){ - return [handler, i+match.length, line] } - // func... - return this[handler](state, path, match, str, i, line) }, - - - parse: function(str, functions){ - - // stage 1: build the object... - var state = {functions} - var res = this.value(state, [], str)[0] - - // stage 2: link the recursive structures... - for(var [a, b] of state.recursive ?? []){ - this.setItem(res, a, this.getItem(res, b)) } - - return res }, -} - - -var deserialize = -module.deserialize = -function(str, functions){ - return eJSON.parse(str, functions) } - - - -//--------------------------------------------------------------------- -// utils... - -var deepCopy = -module.deepCopy = -function(obj){ - return deserialize( - serialize(obj)) } - - -var semiDeepCopy = -module.semiDeepCopy = -function(obj){ - var funcs = [] - return deserialize( - serialize(obj, null, 0, funcs), - funcs) } - - - -/********************************************************************** -* vim:set ts=4 sw=4 nowrap : */ return module }) diff --git a/test.js b/test.js index 2e37e5a..8b51759 100755 --- a/test.js +++ b/test.js @@ -49,6 +49,8 @@ var setups = test.Setups({ return ['0.123', json] }, 'float-b': function(assert){ return ['1.23', json] }, + 'bigint': function(assert){ + return ['999999999999999999999n'] }, // XXX need a way to test this... //'float-a': function(assert){ // return '.123' },