ig-serialize 1.0.1 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,5 +11,71 @@ This extends the default JSON serialization adding the following:
11
11
 
12
12
  ## Motivation
13
13
 
14
+ This was originally built as a companion to a testing module for a
15
+ programming class, illustrating several concepts, including: guaranteed
16
+ clean isolation of data structures via serialization, instrumenting code
17
+ and tooling design, basic parsing, among others.
18
+
19
+
20
+
21
+ ## Installation
22
+
23
+
24
+ ## Introduction
25
+
26
+
27
+ ### Serializing functions
28
+
29
+ Due to how JavaScript is designed it is not possible to trivially and
30
+ fully clone a function with all of it's references, `.serilaize(..)` will
31
+ not attempt to clone any state a function may have, this will lead to
32
+ loosing:
33
+
34
+ - Function closure
35
+ - Attributes set on the function or any of it's prototypes, including the
36
+ `.__proto__` value if it was changed.
37
+
38
+ Thus, care must be taken when serializing structures containing function.
39
+
40
+
41
+ ## API
42
+
43
+ ### `serialize(..)` / `eJSON.stringify(..)`
44
+
45
+ ### `deserialize(..)` / 'eJSON.parse(..)'
46
+
47
+ ### `deepCopy(..)`
48
+
49
+ ### `partialDeepCopy(..)`
50
+
51
+
52
+ ## Format
53
+
54
+ The output of `.serialize(..)` is a strict superset of [standard JSON](https://www.json.org/json-en.html),
55
+ while the input format is a bit more relaxed than in several details.
56
+
57
+ Extensions to JSON:
58
+ - Recursion
59
+ - undefined / NaN
60
+ - BigInt
61
+ - Map / Set
62
+ - Function
63
+
64
+ ### Structural paths
65
+
66
+ ### Recursion
67
+
68
+ ### null types
69
+
70
+ ### BigInt
71
+
72
+ ### Map / Set
73
+
74
+ ### Functions
75
+
76
+
77
+
78
+
79
+
14
80
 
15
81
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ig-serialize",
3
- "version": "1.0.1",
3
+ "version": "1.0.5",
4
4
  "description": "experimental extended json serializaion...",
5
5
  "main": "serialize.js",
6
6
  "scripts": {
package/serialize.js CHANGED
@@ -1,5 +1,41 @@
1
1
  /**********************************************************************
2
2
  *
3
+ * TODO would be useful to split this into:
4
+ * - object traversal using object path
5
+ * Object.walk(..) -- a-la Python's walk(..)
6
+ * Object.graph(..)
7
+ * like .map(..) but deep and paths instead of indexes...
8
+ * - get/set object by path
9
+ * Object.get(..) / Object.set(..)
10
+ *
11
+ * XXX the current path implementation is fully complient to the task at
12
+ * hand but it will not suite the diff task as there is no way to know
13
+ * the meaning of the path element without seeing the object (map/set
14
+ * indexes)...
15
+ * a different path format would suit both tasks and make things more
16
+ * universal:
17
+ * Current:
18
+ * [1, 'a', 5, 0, 3]
19
+ * \ \ +-++ +---------------- Set index (from context)
20
+ * \ \ +------------------- Map index (from context)
21
+ * \ +------------------------ Object key (string)
22
+ * +--------------------------- Array index (from context)
23
+ * Proposed:
24
+ * [1, 'a', ['map', 5, 0], ['set', 3]]
25
+ * \ \ +---------+-+ +------+-+
26
+ * \ \ \ +--- Set index
27
+ * \ \ +-------------- Map index
28
+ * \ +--------------------------- Object key (string)
29
+ * +------------------------------ Array index (number)
30
+ * And/or:
31
+ * [1, ['object', 3], ['map', 5, 0], ['set', 3]]
32
+ * +---------+-+
33
+ * +----- Object attr index
34
+ * The questions is which to use as default for objects, the key or
35
+ * position?
36
+ * (XXX move this note to diff.jx)
37
+ *
38
+ * XXX do a revision of the JSON standard for things I could have forgotten...
3
39
  *
4
40
  *
5
41
  **********************************************************************/
@@ -41,8 +77,12 @@ var debug = {
41
77
 
42
78
  //---------------------------------------------------------------------
43
79
 
80
+ module.STRING_LENGTH_REF = RECURSIVE.length * 8
81
+
82
+
44
83
  //
45
- // serialize(obj[, indent[, depth]])
84
+ // serialize(obj[, options])
85
+ // serialize(obj[, indent[, depth[, options]]])
46
86
  // -> str
47
87
  //
48
88
  // indent can be:
@@ -50,7 +90,7 @@ var debug = {
50
90
  // string - string to use for indenting
51
91
  //
52
92
  //
53
- // _serialize(obj, base_path, seen, indent, depth, functions)
93
+ // _serialize(obj, base_path, seen, indent, depth, options)
54
94
  // -> str
55
95
  //
56
96
  //
@@ -98,12 +138,16 @@ var debug = {
98
138
  // ]
99
139
  //
100
140
  //
101
- // XXX BUG: using non-whitespace as indent breaks the depth of the first
141
+ // XXX BUG?: using non-whitespace as indent breaks the depth of the first
102
142
  // or last elements in sequences
103
143
  // ...breaks .trim*() in Map/Set/Object...
104
144
  var _serialize =
105
145
  module._serialize =
106
- function(obj, path=[], seen=new Map(), indent, depth=0, functions){
146
+ function(obj, path=[], seen=new Map(), indent, depth=0, options={}){
147
+ var string_length_ref =
148
+ options.string_length_ref
149
+ ?? module.STRING_LENGTH_REF
150
+
107
151
  // recursive...
108
152
  var p = seen.get(obj)
109
153
  if(p != null){
@@ -117,14 +161,25 @@ function(obj, path=[], seen=new Map(), indent, depth=0, functions){
117
161
  seen.set(obj, path)
118
162
  // if functions array is given add function to it and store its
119
163
  // index in the serialized data...
120
- if(functions instanceof Array){
121
- functions.push(obj)
122
- obj = functions.length-1 }
164
+ if(options.functions instanceof Array){
165
+ options.functions.push(obj)
166
+ obj = options.functions.length-1 }
123
167
  var s = '('+ obj.toString() +')'
124
168
  return FUNCTION
125
169
  .replace('%', s.length +','+ s) }
126
170
 
171
+ // long strings...
172
+ // NOTE: this saves on output size...
173
+ if(typeof(obj) == 'string'
174
+ && obj.length > string_length_ref){
175
+ seen.set(obj, path) }
176
+ // BigInt...
177
+ if(typeof(obj) == 'bigint'){
178
+ seen.set(obj, path)
179
+ return obj.toString() +'n' }
180
+
127
181
  // atomics...
182
+ // NOTE: these are not stored in seen thus are not re-referenced...
128
183
  if(obj === null){
129
184
  return NULL }
130
185
  if(typeof(obj) != 'object'){
@@ -137,6 +192,8 @@ function(obj, path=[], seen=new Map(), indent, depth=0, functions){
137
192
  INFINITY
138
193
  : obj === -Infinity ?
139
194
  NEG_INFINITY
195
+ // XXX might be a good idea to reference really long strings instead
196
+ // of storing each...
140
197
  : JSON.stringify(obj, null, indent) }
141
198
 
142
199
  // objects...
@@ -152,20 +209,20 @@ function(obj, path=[], seen=new Map(), indent, depth=0, functions){
152
209
  for(var i=0; i < obj.length; i++){
153
210
  elems.push(
154
211
  i in obj ?
155
- _serialize(obj[i], [...path, i], seen, indent, depth+1, functions)
212
+ _serialize(obj[i], [...path, i], seen, indent, depth+1, options)
156
213
  : EMPTY) }
157
214
  } else if(obj instanceof Map){
158
215
  pre = 'Map(['
159
216
  post = '])'
160
217
  elems = [
161
- _serialize([...obj], path, seen, indent, depth, functions)
218
+ _serialize([...obj], path, seen, indent, depth, options)
162
219
  .slice(1, -1)
163
220
  .trim() ]
164
221
  } else if(obj instanceof Set){
165
222
  pre = 'Set(['
166
223
  post = '])'
167
224
  elems = [
168
- _serialize([...obj], path, seen, indent, depth, functions)
225
+ _serialize([...obj], path, seen, indent, depth, options)
169
226
  .slice(1, -1)
170
227
  .trim() ]
171
228
  } else {
@@ -175,7 +232,7 @@ function(obj, path=[], seen=new Map(), indent, depth=0, functions){
175
232
  elems.push(`${
176
233
  JSON.stringify(k)
177
234
  }:${ indent != null ? ' ' : '' }${
178
- _serialize(v, [...path, k], seen, indent, depth+1, functions)
235
+ _serialize(v, [...path, k], seen, indent, depth+1, options)
179
236
  // relevant for pretty-printing only...
180
237
  .trimLeft()
181
238
  }`) } }
@@ -196,8 +253,8 @@ function(obj, path=[], seen=new Map(), indent, depth=0, functions){
196
253
  // user interface...
197
254
  var serialize =
198
255
  module.serialize =
199
- function(obj, indent, depth=0, functions){
200
- return _serialize(obj, [], new Map(), indent, depth, functions) }
256
+ function(obj, indent, depth=0, options){
257
+ return _serialize(obj, [], new Map(), indent, depth, options) }
201
258
 
202
259
 
203
260
  // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -211,7 +268,7 @@ module.eJSON = {
211
268
 
212
269
  0: 'number', 1: 'number', 2: 'number', 3: 'number', 4: 'number',
213
270
  5: 'number', 6: 'number', 7: 'number', 8: 'number', 9: 'number',
214
- '.': 'number', '-': 'number',
271
+ '.': 'number', '-': 'number', '+': 'number',
215
272
 
216
273
  '[': 'array',
217
274
  '{': 'object',
@@ -351,12 +408,37 @@ module.eJSON = {
351
408
  && str.slice(i, i+'-Infinity'.length) == '-Infinity'){
352
409
  return [-Infinity, i+'-Infinity'.length, line] }
353
410
  // numbers...
354
- var j = i+1
411
+ // XXX do we need to handle the odd case of 077 vs 088???
412
+ var j = i
413
+ var mode = 'dec'
414
+ if(str[j] == '0'
415
+ && 'xXbBoO'.includes(str[j+1])){
416
+ mode = str[j+1]
417
+ j++ }
418
+ j++
355
419
  while(j < str.length
356
420
  && (str[j] == '.'
421
+ // dec/oct/bin...
422
+ // XXX do we need to be pedantic and check the rest
423
+ // of the modes explicitly???
357
424
  || (str[j] >= '0'
358
- && str[j] <= '9'))){
425
+ && str[j] <= '9')
426
+ // hex...
427
+ || (mode == 'x'
428
+ && ((str[j] >= 'a'
429
+ && str[j] <= 'f')
430
+ || (str[j] >= 'A'
431
+ && str[j] <= 'F')))
432
+ // exponent...
433
+ || str[j] == 'e'
434
+ || str[j] == 'E')){
435
+ if('eE'.includes(str[j])
436
+ && '+-'.includes(str[j+1])){
437
+ j++ }
359
438
  j++ }
439
+ // BigInt...
440
+ if(str[j] == 'n'){
441
+ return [ BigInt(str.slice(i, j)), j+1, line ] }
360
442
  return [ str.slice(i, j)*1, j, line ] },
361
443
  // XXX TEST count \\n
362
444
  string: function(state, path, match, str, i, line){
@@ -381,7 +463,10 @@ module.eJSON = {
381
463
  if(j == str.length
382
464
  && str[j-1] != match){
383
465
  this.error('Unexpected end of input wile looking fot "'+ match +'".', str, i, line) }
384
- return [ str.slice(i+1, j), j+1, line ] },
466
+ // NOTE: this is cheating -- instead of dancing around and
467
+ // replicating the full functionality of JSON.parse(..) we
468
+ // are simply falling back on it, as we did when serializing.
469
+ return [ JSON.parse(`"${str.slice(i+1, j)}"`), j+1, line ] },
385
470
  identifier: function(state, path, match, str, i, line){
386
471
  debug.lex('identifier', str, i, line)
387
472
  if(!/[a-zA-Z_]/.test(str[i])){
@@ -543,7 +628,6 @@ module.eJSON = {
543
628
  // NOTE: this uses eval(..) so care must be taken when enabling this...
544
629
  func: function(state, path, match, str, i, line){
545
630
  if(state.functions == null){
546
- console.log('---', state)
547
631
  this.error('Deserializing functions disabled.', str, i, line) }
548
632
 
549
633
  debug.lex('function', str, i, line)
@@ -555,7 +639,7 @@ module.eJSON = {
555
639
  this.error('Expected "," got "'+ str[i] +'"', str, i, line) }
556
640
  i++
557
641
 
558
- // func ref...
642
+ // func index...
559
643
  if(state.functions instanceof Array){
560
644
  var [n, i, line] = this.number(state, path, str[i+1], str, i+1, line)
561
645
  res = state.functions[n]
@@ -604,10 +688,10 @@ module.eJSON = {
604
688
  return this[handler](state, path, match, str, i, line) },
605
689
 
606
690
 
607
- parse: function(str, functions){
691
+ parse: function(str, options={}){
608
692
 
609
693
  // stage 1: build the object...
610
- var state = {functions}
694
+ var state = {functions: options.functions}
611
695
  var res = this.value(state, [], str)[0]
612
696
 
613
697
  // stage 2: link the recursive structures...
@@ -615,13 +699,20 @@ module.eJSON = {
615
699
  this.setItem(res, a, this.getItem(res, b)) }
616
700
 
617
701
  return res },
702
+
703
+ // to comply with POLS...
704
+ stringify: serialize,
618
705
  }
619
706
 
620
707
 
621
708
  var deserialize =
622
709
  module.deserialize =
623
- function(str, functions){
624
- return eJSON.parse(str, functions) }
710
+ function(str, options){
711
+ options =
712
+ options === true ?
713
+ {functions: true}
714
+ : options
715
+ return eJSON.parse(str, options) }
625
716
 
626
717
 
627
718
 
@@ -630,18 +721,20 @@ function(str, functions){
630
721
 
631
722
  var deepCopy =
632
723
  module.deepCopy =
633
- function(obj, functions){
724
+ function(obj, funcs){
725
+ var options = {functions: funcs}
634
726
  return deserialize(
635
- serialize(obj, null, 0, functions),
636
- functions) }
727
+ serialize(obj, null, 0, options),
728
+ options) }
637
729
 
638
730
 
639
731
  var partialDeepCopy =
640
732
  module.partialDeepCopy =
641
733
  function(obj, funcs=[]){
734
+ var options = {functions: funcs}
642
735
  return deserialize(
643
- serialize(obj, null, 0, funcs),
644
- funcs) }
736
+ serialize(obj, null, 0, options),
737
+ options) }
645
738
 
646
739
 
647
740
 
package/test.js CHANGED
@@ -26,6 +26,8 @@ var eJSON = require('./serialize')
26
26
  var json = true
27
27
  var ejson = false
28
28
 
29
+ var pre_cycle = true
30
+
29
31
  // XXX test whitespace handling...
30
32
  var setups = test.Setups({
31
33
  'true': function(assert){
@@ -43,17 +45,23 @@ var setups = test.Setups({
43
45
  return ['123', json] },
44
46
  'number-neg': function(assert){
45
47
  return ['-123', json] },
48
+ //'number-pos': function(assert){
49
+ // return ['+123', json] },
50
+ 'number-exp': function(assert){
51
+ return ['1e+100', json] },
46
52
  'number-zero': function(assert){
47
53
  return ['0', json] },
48
54
  'float-a': function(assert){
49
55
  return ['0.123', json] },
50
56
  'float-b': function(assert){
51
57
  return ['1.23', json] },
58
+ 'bigint': function(assert){
59
+ return ['999999999999999999999n'] },
52
60
  // XXX need a way to test this...
53
61
  //'float-a': function(assert){
54
- // return '.123' },
62
+ // return ['.123'] },
55
63
  //'float-c': function(assert){
56
- // return '123.' },
64
+ // return ['123.'] },
57
65
  // XXX also test:
58
66
  // hex/bin/orc/...
59
67
  Infinity: function(assert){
@@ -64,6 +72,10 @@ var setups = test.Setups({
64
72
  // XXX also test diffrerent quotations...
65
73
  string: function(assert){
66
74
  return ['"string"', json] },
75
+ 'string-empty': function(assert){
76
+ return ['""', json] },
77
+ 'string-multiline': function(assert){
78
+ return ['"string\\nstring\\nstring"', json] },
67
79
 
68
80
  'array-empty': function(assert){
69
81
  return ['[]', json] },
@@ -99,7 +111,6 @@ var setups = test.Setups({
99
111
  return ['Map([[<RECURSIVE[]>,"value"]])'] },
100
112
  'map-recursive-value': function(assert){
101
113
  return ['Map([["key",<RECURSIVE[]>]])'] },
102
-
103
114
  })
104
115
 
105
116
  test.Modifiers({
@@ -163,7 +174,6 @@ test.Tests({
163
174
  // XXX
164
175
  },
165
176
 
166
- //* XXX ERR
167
177
  'partial-deep-copy': function(assert, [setup]){
168
178
  var obj = eJSON.deserialize(setup, true)
169
179
  var funcs = []
@@ -177,17 +187,85 @@ test.Tests({
177
187
  //*/
178
188
  })
179
189
 
180
- /* XXX these break things...
181
190
  test.Cases({
182
- 'deep-copy-function': function(assert, [setup]){
183
- // XXX check function isolation...
184
- },
185
-
186
- 'partial-deep-copy-function': function(assert, [setup]){
187
- // XXX check function isolation...
188
- },
191
+ //
192
+ // Format:
193
+ // [
194
+ // [ "<extended-syntax>", "<JSON-syntax>" ],
195
+ // ...
196
+ // ]
197
+ //
198
+ // NOTE: these syntax variants are not output by .serialize(..) this it
199
+ // is less critical to test them in the main loop.
200
+ // XXX though it would be nice to do so...
201
+ tests: [
202
+ // numbers/floats...
203
+ ['.123', '0.123'],
204
+ ['123.', '123'],
205
+ ['+123', '123'],
206
+ ['123e100', '123e+100'],
207
+ ['0xff', '255'],
208
+
209
+ // string quotes...
210
+ ["'abc'", '"abc"'],
211
+ ['`abc`', '"abc"'],
212
+
213
+ // arrays...
214
+ ['[1,2,]', '[1,2]'],
215
+
216
+ // sparse arrays...
217
+ ['[<empty>]', '[,]'],
218
+ ['[1,2,<empty>]', '[1,2,,]'],
219
+ ['[1,2,<empty>]', '[1,2,<empty>,]'],
220
+ ],
221
+ 'syntax-simplifications': function(assert){
222
+ var aa, bb
223
+ for(var [a, b] of this.tests){
224
+ assert(eJSON.serialize(aa = eJSON.deserialize(a)) == eJSON.serialize(bb = eJSON.deserialize(b)),
225
+ `"${ a }" and "${ b }" should deserialize to the samve value.`,
226
+ 'got:', aa, 'and', bb, 'resp.') } },
227
+
228
+ _make_object_with_methods: function(){
229
+ var in_closure = 123
230
+ return {
231
+ stateful: function(){
232
+ return typeof(in_closure) != 'undefined' ?
233
+ 'state_retained'
234
+ : 'state_lost' },
235
+ stateless: function(){
236
+ return 'stateless' },
237
+ } },
238
+ 'deep-copy-function': function(assert){
239
+ var obj = this._make_object_with_methods()
240
+ var obj_copy = eJSON.deepCopy(obj, true)
241
+
242
+ // sanity checks...
243
+ assert(obj.stateless() == 'stateless')
244
+ assert(obj.stateful() == 'state_retained')
245
+ assert(obj_copy.stateless() == 'stateless')
246
+
247
+ // context should be lost...
248
+ assert(
249
+ obj_copy.stateful() == 'state_lost',
250
+ 'Function closure not lost.')
251
+ assert(obj.stateful !== obj_copy.stateful,
252
+ 'Function objects retained.') },
253
+ 'partial-deep-copy-function': function(assert){
254
+ var obj = this._make_object_with_methods()
255
+ var obj_copy = eJSON.partialDeepCopy(obj)
256
+
257
+ // sanity checks...
258
+ assert(obj.stateless() == 'stateless')
259
+ assert(obj.stateful() == 'state_retained')
260
+ assert(obj_copy.stateless() == 'stateless')
261
+
262
+ // context should be retained...
263
+ assert(
264
+ obj_copy.stateful() == 'state_retained',
265
+ 'Function closure lost.')
266
+ assert(obj.stateful === obj_copy.stateful,
267
+ 'Function objects not retained.') },
189
268
  })
190
- //*/
191
269
 
192
270
 
193
271
  //---------------------------------------------------------------------
package/serialize2.js DELETED
@@ -1,645 +0,0 @@
1
- /**********************************************************************
2
- *
3
- *
4
- *
5
- **********************************************************************/
6
- ((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
7
- (function(require){ var module={} // make module AMD/node compatible...
8
- /*********************************************************************/
9
-
10
-
11
- var EMPTY = '<empty>'
12
- var NULL = 'null'
13
- var UNDEFINED = 'undefined'
14
- var NAN = 'NaN'
15
- var INFINITY = 'Infinity'
16
- var NEG_INFINITY = '-Infinity'
17
-
18
- var RECURSIVE = '<RECURSIVE%>'
19
-
20
- var FUNCTION = '<FUNCTION[%]>'
21
-
22
-
23
-
24
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
25
-
26
- module.DEBUG = false
27
-
28
- var DEBUG_PREFIX = '---'
29
-
30
- var debug = {
31
- context: 16,
32
-
33
- log: function(...args){
34
- if(!module.DEBUG){
35
- return }
36
- return console.log(DEBUG_PREFIX, ...arguments) },
37
- lex: function(name, str, i, line){
38
- return this.log(name +':', str.slice(i, i+this.context) +'...') },
39
- }
40
-
41
-
42
- //---------------------------------------------------------------------
43
-
44
- //
45
- // serialize(obj[, indent[, depth]])
46
- // -> str
47
- //
48
- // indent can be:
49
- // number - number of spaces to use for indent
50
- // string - string to use for indenting
51
- //
52
- //
53
- // _serialize(obj, base_path, seen, indent, depth, functions)
54
- // -> str
55
- //
56
- //
57
- // Paths
58
- // A path is a chain of indexes/keys leading to a specific object in
59
- // tree.
60
- //
61
- // the root object is referenced by an empty path array.
62
- //
63
- // For sets, positional indexes are used, i.e. a set is treated like
64
- // an array ov values.
65
- //
66
- // For maps a two number index is used with the first being the position
67
- // of the item and the second indicated if the target is the key or the
68
- // value. i.e. a map is treated like an array of key value pairs.
69
- //
70
- //
71
- // Examples:
72
- // Object paths
73
- // ---------------------------------------------------------------
74
- // obj = [ <- []
75
- // null, <- [0]
76
- // [
77
- // 'a',
78
- // 'b', <- [1, 1]
79
- // 'c',
80
- // ],
81
- // Set([
82
- // 1,
83
- // 2,
84
- // 3, <- [2, 2]
85
- // ]),
86
- // Map([i <- [3] +----- index of elemnt in map
87
- // [ / +--- 0 means key
88
- // 'key', <- [3, 0, 0]
89
- // 'value' <- [3, 0, 1]
90
- // ], +--- 1 means value
91
- // [
92
- // [
93
- // 123 <- [3, 1, 0, 0]
94
- // ],
95
- // 'got tired of thinking up names',
96
- // ],
97
- // ]),
98
- // ]
99
- //
100
- //
101
- // XXX BUG: using non-whitespace as indent breaks the depth of the first
102
- // or last elements in sequences
103
- // ...breaks .trim*() in Map/Set/Object...
104
- var _serialize =
105
- module._serialize =
106
- function(obj, path=[], seen=new Map(), indent, depth=0, functions){
107
- // recursive...
108
- var p = seen.get(obj)
109
- if(p != null){
110
- // NOTE: _serialize(..) is always printed flat here, regardless of indent/depth...
111
- return RECURSIVE.replace('%', _serialize(p)) }
112
-
113
- // functions...
114
- // NOTE: we are storing function length to avoid parsing the function...
115
- // NOTE: storing length is a potential attack vector...
116
- if(typeof(obj) == 'function'){
117
- seen.set(obj, path)
118
- // if functions array is given add function to it and store its
119
- // index in the serialized data...
120
- if(functions != null){
121
- functions.push(obj)
122
- obj = functions.length-1 }
123
- var s = '('+ obj.toString() +')'
124
- return FUNCTION
125
- .replace('%', s.length +','+ s) }
126
-
127
- // atomics...
128
- if(obj === null){
129
- return NULL }
130
- if(typeof(obj) != 'object'){
131
- return typeof(obj) == 'number'
132
- && isNaN(obj) ?
133
- NAN
134
- : obj === undefined ?
135
- UNDEFINED
136
- : obj === Infinity ?
137
- INFINITY
138
- : obj === -Infinity ?
139
- NEG_INFINITY
140
- : JSON.stringify(obj, null, indent) }
141
-
142
- // objects...
143
- seen.set(obj, path)
144
-
145
- var elems = []
146
- var pre = ''
147
- var join = ','
148
- var post = ''
149
- if(obj instanceof Array){
150
- pre = '['
151
- post = ']'
152
- for(var i=0; i < obj.length; i++){
153
- elems.push(
154
- i in obj ?
155
- _serialize(obj[i], [...path, i], seen, indent, depth+1, functions)
156
- : EMPTY) }
157
- } else if(obj instanceof Map){
158
- pre = 'Map(['
159
- post = '])'
160
- elems = [
161
- _serialize([...obj], path, seen, indent, depth, functions)
162
- .slice(1, -1)
163
- .trim() ]
164
- } else if(obj instanceof Set){
165
- pre = 'Set(['
166
- post = '])'
167
- elems = [
168
- _serialize([...obj], path, seen, indent, depth, functions)
169
- .slice(1, -1)
170
- .trim() ]
171
- } else {
172
- pre = '{'
173
- post = '}'
174
- for(var [k, v] of Object.entries(obj)){
175
- elems.push(`${
176
- JSON.stringify(k)
177
- }:${ indent != null ? ' ' : '' }${
178
- _serialize(v, [...path, k], seen, indent, depth+1, functions)
179
- // relevant for pretty-printing only...
180
- .trimLeft()
181
- }`) } }
182
-
183
- // handle indent...
184
- if(indent != null){
185
- i = indent.repeat(depth)
186
- s = i + indent
187
- if(elems.length > 0){
188
- pre = pre + '\n' + s
189
- post = '\n' + i + post
190
- // XXX set limit for number of elements to keep horizontal...
191
- // ...also account for element length...
192
- join = join + '\n' + s } }
193
-
194
- return pre+ elems.join(join) +post }
195
-
196
- // user interface...
197
- var serialize =
198
- module.serialize =
199
- function(obj, indent, depth=0, functions){
200
- return _serialize(obj, [], new Map(), indent, depth, functions) }
201
-
202
-
203
- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
204
-
205
- // XXX better error handling...
206
- // XXX try and make this single stage (see notes for : .recursive(..))
207
- var eJSON =
208
- module.eJSON = {
209
- chars: {
210
- '"': 'string', "'": 'string', '`': 'string',
211
-
212
- 0: 'number', 1: 'number', 2: 'number', 3: 'number', 4: 'number',
213
- 5: 'number', 6: 'number', 7: 'number', 8: 'number', 9: 'number',
214
- '.': 'number', '-': 'number',
215
-
216
- '[': 'array',
217
- '{': 'object',
218
- },
219
- words: {
220
- true: true,
221
- false: false,
222
-
223
- Infinity: 'number',
224
-
225
- 'Set(': 'set',
226
- 'Map(': 'map',
227
-
228
- null: null,
229
- undefined: undefined,
230
- NaN: NaN,
231
-
232
- '<empty>': 'empty',
233
- '<RECURSIVE': 'recursive',
234
-
235
- '<FUNCTION[': 'func',
236
- },
237
-
238
-
239
- // generic helpers...
240
- //
241
- // XXX need to keep the context constrained to one line...
242
- context_size: 16,
243
- error: function(msg, str, i, line){
244
- var pre = i
245
- var post = i
246
- while(pre > 0
247
- && i - pre <= this.context_size
248
- && str[pre] != '\n'){
249
- pre-- }
250
- while(post < str.length
251
- && post - i <= this.context_size
252
- && str[post] != '\n'){
253
- post++ }
254
- throw new SyntaxError(`${ msg }\n`
255
- +` ${line}: ${ str.slice(pre, post) }\n`
256
- +` ${ ' '.repeat( i - pre + ((line + ': ').length) ) }^`) },
257
-
258
- //
259
- // ._getItem(obj, path)
260
- // -> [path, obj]
261
- //
262
- // NOTE: we are returning path here because we need to distinguish
263
- // incomplete paths that we can infer the container from and
264
- // complete paths, e.g:
265
- // For
266
- // m = [new Map([[1,2], [2,3]])]
267
- // Both of the below will return the map and the correct path ([0]):
268
- // eJSON._getItem(m, [0, 1])
269
- // eJSON._getItem(m, [0])
270
- // But we need to destinguish between the two cases when
271
- // writing to a map (see: .setItem(..))
272
- _getItem: function(obj, path){
273
- for(var i=0; i<path.length; i++){
274
- var k = path[i]
275
- // speacial case: incomplete map index...
276
- if( obj instanceof Map
277
- && i == path.length-1){
278
- return [path.slice(0, -1), obj] }
279
-
280
- obj = obj instanceof Set ?
281
- [...obj][k]
282
- : obj instanceof Map ?
283
- [...obj][k][path[++i]]
284
- : obj[k] }
285
- return [path, obj] },
286
- //
287
- // .getItem(obj, path)
288
- // -> obj
289
- //
290
- // NOTE: this is a POLS wrapper of ._getItem(..)
291
- getItem: function(obj, path){
292
- return this._getItem(...arguments)[1] },
293
- // NOTE: this behaves in a similar way to normal key value assignment,
294
- // i.e. replacing items if they exist, this is not a problem here
295
- // as in the general case we are assigning over a placeholder
296
- // object...
297
- // NOTE: as a side-effect this maintains element oreder even in object
298
- // with no direct concept of element order, like objects, sets and
299
- // maps.
300
- // NOTE: some operations envolve rewriting the container elements so
301
- // are not as fast, namely writing set elements and map keys.
302
- setItem: function(obj, path, value){
303
- var [p, parent] = this._getItem(obj, path.slice(0, -1))
304
- var k = path.at(-1)
305
- if(parent instanceof Set){
306
- var elems = [...parent]
307
- elems[k] = value
308
- // we cant to keep the order...
309
- parent.clear()
310
- for(var e of elems){
311
- parent.add(e) }
312
- } else if(parent instanceof Map){
313
- if(path.length-2 !== p.length){
314
- throw new Error('.setItem(..): incomplete path.') }
315
- var i = path.at(-2)
316
- // replace the index...
317
- if(k == 0){
318
- var elems = [...parent]
319
- elems[i][0] = value
320
- parent.clear()
321
- for(var e of elems){
322
- parent.set(...e) }
323
- // set the value...
324
- } else {
325
- parent.set([...parent][i][0], value) }
326
- } else {
327
- parent[k] = value }
328
- return value },
329
-
330
-
331
-
332
- WHITESPACE: ' \t\n',
333
- skipWhitespace: function(str, i, line){
334
- while(i < str.length
335
- && this.WHITESPACE.includes(str[i])){
336
- if(str[i] == '\n'){
337
- line++ }
338
- i++ }
339
- return [i, line] },
340
-
341
- //
342
- // .handler(match, str, i, line)
343
- // -> [value, i, line]
344
- //
345
- number: function(state, path, match, str, i, line){
346
- debug.lex('number', str, i, line)
347
- // special cases..,
348
- if(match == 'Infinity'){
349
- return [Infinity, i+'Infinity'.length, line] }
350
- if(match == '-'
351
- && str.slice(i, i+'-Infinity'.length) == '-Infinity'){
352
- return [-Infinity, i+'-Infinity'.length, line] }
353
- // numbers...
354
- var j = i+1
355
- while(j < str.length
356
- && (str[j] == '.'
357
- || (str[j] >= '0'
358
- && str[j] <= '9'))){
359
- j++ }
360
- return [ str.slice(i, j)*1, j, line ] },
361
- // XXX TEST count \\n
362
- string: function(state, path, match, str, i, line){
363
- debug.lex('string', str, i, line)
364
- var j = i+1
365
- while(j < str.length
366
- && str[j] != match){
367
- // newlines...
368
- if(str[j] == '\n'){
369
- line++ }
370
- // escaped newlines...
371
- if(str[j] == '\\'
372
- && j+1 < str.length
373
- && str[j+1] == 'n'){
374
- line++ }
375
- // skip escaped quotes...
376
- if(str[j] == '\\'
377
- && j+1 < str.length
378
- && str[j+1] == match){
379
- j++ }
380
- j++ }
381
- if(j == str.length
382
- && str[j-1] != match){
383
- this.error('Unexpected end of input wile looking fot "'+ match +'".', str, i, line) }
384
- return [ str.slice(i+1, j), j+1, line ] },
385
- identifier: function(state, path, match, str, i, line){
386
- debug.lex('identifier', str, i, line)
387
- if(!/[a-zA-Z_]/.test(str[i])){
388
- this.error('Not an identifier: "'+ str[i] +'"', str, i, line) }
389
- var j = i+1
390
- while(j < str.length
391
- && /[a-zA-Z0-9_]/.test(str[j])){
392
- j++ }
393
- return [ str.slice(i, j), j, line ] },
394
-
395
- //
396
- // handler(res, index, str, i, line)
397
- // -> [res, i, line]
398
- //
399
- sequence: function(state, path, str, i, line, end, handler, initial=[]){
400
- var index = 0
401
- while(i < str.length){
402
- ;[i, line] = this.skipWhitespace(str, i, line)
403
-
404
- // done...
405
- if(str.slice(i, i+end.length) == end){
406
- return [initial, i+end.length, line] }
407
-
408
- // empty...
409
- if(str[i] == ','){
410
- index++
411
- i++
412
- // XXX this feels hackish -- can this be deligated to the handler???
413
- initial instanceof Array
414
- && initial.length++
415
- continue }
416
- if(str.slice(i, i+EMPTY.length) == EMPTY){
417
- index++
418
- i += EMPTY.length
419
- if(str[i] == ','){
420
- i++ }
421
- // XXX this feels hackish -- can this be deligated to the handler???
422
- initial instanceof Array
423
- && initial.length++
424
- continue }
425
-
426
- // end of input...
427
- if(i >= str.length-1){
428
- break }
429
-
430
- // value...
431
- ;[initial, i, line] = handler.call(this, initial, index, str, i, line)
432
-
433
- ;[i, line] = this.skipWhitespace(str, i, line)
434
- if(str[i] == ','){
435
- i++ }
436
- index++ }
437
-
438
- // XXX better message -- show starting seq...
439
- this.error('Unexpected end of input wile looking for "'+ end +'".', str, i, line) },
440
-
441
- array: function(state, path, match, str, i, line){
442
- debug.lex('array', str, i, line)
443
- return this.sequence(
444
- state, path, str, i+1, line,
445
- ']',
446
- function(res, index, str, i, line){
447
- var obj
448
- ;[obj, i, line] = this.value(state, [...path, index], str, i, line)
449
- res[index] = obj
450
- return [res, i, line] }) },
451
- object: function(state, path, match, str, i, line){
452
- debug.lex('object', str, i, line)
453
- return this.sequence(
454
- state, path, str, i+1, line,
455
- '}',
456
- function(res, index, str, i, line){
457
- var obj, key
458
- // key...
459
- ;[key, i, line] = '\'"`'.includes(str[i]) ?
460
- this.string(state, path, str[i], str, i, line)
461
- : this.identifier(state, path, str[i], str, i, line)
462
-
463
- // ':'...
464
- ;[i, line] = this.skipWhitespace(str, i, line)
465
- if(str[i] != ':'){
466
- this.error('Expected ":", got "'+ str[i] +'".', str, i, line) }
467
- i++
468
- ;[i, line] = this.skipWhitespace(str, i, line)
469
-
470
- // value...
471
- ;[obj, i, line] = this.value(state, [...path, key], str, i, line)
472
- res[key] = obj
473
- return [res, i, line] },
474
- {}) },
475
-
476
- set: function(state, path, match, str, i, line){
477
- debug.lex('set', str, i, line)
478
- i += match.length
479
- ;[res, i, line] = this.sequence(
480
- state, path,
481
- str, i+1, line,
482
- ']',
483
- function(res, index, str, i, line){
484
- var obj
485
- ;[obj, i, line] = this.value(
486
- state,
487
- [...path, index],
488
- str, i, line)
489
- res.add(obj)
490
- return [res, i, line] },
491
- new Set())
492
- if(str[i] != ')'){
493
- this.error('Expected ")", got "'+ str[i] +'".', str, i, line) }
494
- return [res, i+1, line] },
495
- map: function(state, path, match, str, i, line){
496
- debug.lex('map', str, i, line)
497
- i += match.length
498
- ;[res, i, line] = this.sequence(
499
- state, path,
500
- str, i+1, line,
501
- ']',
502
- function(res, index, str, i, line){
503
- debug.lex(' map-content', str, i, line)
504
- var obj
505
- ;[[key, value], i, line] = this.value(
506
- state,
507
- [...path, index],
508
- str, i, line)
509
- res.set(key, value)
510
- return [res, i, line] },
511
- new Map())
512
- if(str[i] != ')'){
513
- this.error('Expected ")", got "'+ str[i] +'".', str, i, line) }
514
- return [res, i+1, line] },
515
-
516
- // XXX should this be done inline or on a separate stage?
517
- // for inline we need these:
518
- // - all containers must be placed as soon as they are created
519
- // "return" before the items are processed...
520
- // - containers must be "filled" as the items are parsed
521
- // like .array(..) and .object(..) but not like .map(..) and .set(..)
522
- // - thread the root object...
523
- // alternatively, for a two stage process we need to:
524
- // - have a mechanism to write placeholders
525
- // - remember their positions (paths)
526
- // - build/get said path
527
- // XXX another strategy would be to have an path-object map and
528
- // directly reference that -- this would eliminate the need for
529
- // stage two... (XXX TEST)
530
- // ...need to use serialized paths as keys...
531
- recursive: function(state, path, match, str, i, line){
532
- debug.lex('recursive', str, i, line)
533
- return this.sequence(
534
- state, path, str, i+match.length, line,
535
- '>',
536
- function(res, index, str, i, line){
537
- var obj
538
- ;[obj, i, line] = this.array(state, [...path, index], '[', str, i, line)
539
- var rec = state.recursive ??= []
540
- rec.push([path, obj])
541
- return [{}, i, line] }) },
542
-
543
- // NOTE: this uses eval(..) so care must be taken when enabling this...
544
- func: function(state, path, match, str, i, line){
545
- if(state.functions == null){
546
- this.error('Deserializing functions disabled.', str, i, line) }
547
-
548
- debug.lex('function', str, i, line)
549
- var res
550
- // length...
551
- i += match.length
552
- var [l, i, line] = this.number(state, path, str[i], str, i, line)
553
- if(str[i] != ','){
554
- this.error('Expected "," got "'+ str[i] +'"', str, i, line) }
555
- i++
556
-
557
- // func ref...
558
- if(state.functions instanceof Array){
559
- var [n, i, line] = this.number(state, path, str[i+1], str, i+1, line)
560
- res = state.functions[n]
561
- // func code...
562
- } else {
563
- var code = str.slice(i, i+l)
564
- i += l
565
- line += code.split(/\n/g).length
566
- if(str.slice(i, i+2) == ']>'){
567
- res = eval?.(code) } }
568
-
569
- if(str.slice(i, i+2) != ']>'){
570
- this.error('Expected "]>" got "'+ str.slice(i, i+2) +'"', str, i, line) }
571
- return [res, i+2, line] },
572
-
573
- value: function(state, path, str, i=0, line=0){
574
-
575
- ;[i, line] = this.skipWhitespace(str, i, line)
576
-
577
- // get handler...
578
- var NONE = {}
579
- var handler = NONE
580
- var match = str[i]
581
- if(match in this.chars){
582
- handler = this.chars[match]
583
- } else {
584
- for(match in this.words){
585
- if(match == str.slice(i, i+match.length)){
586
- handler = this.words[match]
587
- break } } }
588
-
589
- // syntax error...
590
- if(handler === NONE){
591
- var context = 8
592
- var pre = Math.max(i-context, 0)
593
- var post = Math.min(i+context, str.length)
594
- this.error(`Unexpected: ${ str.slice(i, i+10) }`, str, i, line) }
595
-
596
- // value...
597
- if(typeof(handler) != 'string'){
598
- return [handler, i+match.length, line] }
599
- // func...
600
- return this[handler](state, path, match, str, i, line) },
601
-
602
-
603
- parse: function(str, functions){
604
-
605
- // stage 1: build the object...
606
- var state = {functions}
607
- var res = this.value(state, [], str)[0]
608
-
609
- // stage 2: link the recursive structures...
610
- for(var [a, b] of state.recursive ?? []){
611
- this.setItem(res, a, this.getItem(res, b)) }
612
-
613
- return res },
614
- }
615
-
616
-
617
- var deserialize =
618
- module.deserialize =
619
- function(str, functions){
620
- return eJSON.parse(str, functions) }
621
-
622
-
623
-
624
- //---------------------------------------------------------------------
625
- // utils...
626
-
627
- var deepCopy =
628
- module.deepCopy =
629
- function(obj){
630
- return deserialize(
631
- serialize(obj)) }
632
-
633
-
634
- var semiDeepCopy =
635
- module.semiDeepCopy =
636
- function(obj){
637
- var funcs = []
638
- return deserialize(
639
- serialize(obj, null, 0, funcs),
640
- funcs) }
641
-
642
-
643
-
644
- /**********************************************************************
645
- * vim:set ts=4 sw=4 nowrap : */ return module })