]> gerrit.simantics Code Review - simantics/district.git/blob - org.simantics.maps.server/node/node-v4.8.0-win-x64/node_modules/npm/node_modules/read-package-json/node_modules/json-parse-helpfulerror/node_modules/jju/lib/document.js
Adding integrated tile server
[simantics/district.git] / org.simantics.maps.server / node / node-v4.8.0-win-x64 / node_modules / npm / node_modules / read-package-json / node_modules / json-parse-helpfulerror / node_modules / jju / lib / document.js
1 /*
2  * Author: Alex Kocharin <alex@kocharin.ru>
3  * GIT: https://github.com/rlidwka/jju
4  * License: WTFPL, grab your copy here: http://www.wtfpl.net/txt/copying/
5  */
6
7 var assert = require('assert')
8 var tokenize = require('./parse').tokenize
9 var stringify = require('./stringify').stringify
10 var analyze = require('./analyze').analyze
11
12 function isObject(x) {
13   return typeof(x) === 'object' && x !== null
14 }
15
16 function value_to_tokenlist(value, stack, options, is_key, indent) {
17   options = Object.create(options)
18   options._stringify_key = !!is_key
19
20   if (indent) {
21     options._prefix = indent.prefix.map(function(x) {
22       return x.raw
23     }).join('')
24   }
25
26   if (options._splitMin == null) options._splitMin = 0
27   if (options._splitMax == null) options._splitMax = 0
28
29   var stringified = stringify(value, options)
30
31   if (is_key) {
32     return [ { raw: stringified, type: 'key', stack: stack, value: value } ]
33   }
34
35   options._addstack = stack
36   var result = tokenize(stringified, {
37     _addstack: stack,
38   })
39   result.data = null
40   return result
41 }
42
43 // '1.2.3' -> ['1','2','3']
44 function arg_to_path(path) {
45   // array indexes
46   if (typeof(path) === 'number') path = String(path)
47
48   if (path === '') path = []
49   if (typeof(path) === 'string') path = path.split('.')
50
51   if (!Array.isArray(path)) throw Error('Invalid path type, string or array expected')
52   return path
53 }
54
55 // returns new [begin, end] or false if not found
56 //
57 //          {x:3, xxx: 111, y: [111,  {q: 1, e: 2}  ,333]  }
58 // f('y',0) returns this       B^^^^^^^^^^^^^^^^^^^^^^^^E
59 // then f('1',1) would reduce it to   B^^^^^^^^^^E
60 function find_element_in_tokenlist(element, lvl, tokens, begin, end) {
61   while(tokens[begin].stack[lvl] != element) {
62     if (begin++ >= end) return false
63   }
64   while(tokens[end].stack[lvl] != element) {
65     if (end-- < begin) return false
66   }
67   return [begin, end]
68 }
69
70 function is_whitespace(token_type) {
71   return token_type === 'whitespace'
72       || token_type === 'newline'
73       || token_type === 'comment'
74 }
75
76 function find_first_non_ws_token(tokens, begin, end) {
77   while(is_whitespace(tokens[begin].type)) {
78     if (begin++ >= end) return false
79   }
80   return begin
81 }
82
83 function find_last_non_ws_token(tokens, begin, end) {
84   while(is_whitespace(tokens[end].type)) {
85     if (end-- < begin) return false
86   }
87   return end
88 }
89
90 /*
91  * when appending a new element of an object/array, we are trying to
92  * figure out the style used on the previous element
93  *
94  * return {prefix, sep1, sep2, suffix}
95  *
96  *      '    "key" :  "element"    \r\n'
97  * prefix^^^^ sep1^ ^^sep2     ^^^^^^^^suffix
98  *
99  * begin - the beginning of the object/array
100  * end - last token of the last element (value or comma usually)
101  */
102 function detect_indent_style(tokens, is_array, begin, end, level) {
103   var result = {
104     sep1: [],
105     sep2: [],
106     suffix: [],
107     prefix: [],
108     newline: [],
109   }
110
111   if (tokens[end].type === 'separator' && tokens[end].stack.length !== level+1 && tokens[end].raw !== ',') {
112     // either a beginning of the array (no last element) or other weird situation
113     //
114     // just return defaults
115     return result
116   }
117
118   //                              ' "key"  : "value"  ,'
119   // skipping last separator, we're now here        ^^
120   if (tokens[end].type === 'separator')
121     end = find_last_non_ws_token(tokens, begin, end - 1)
122   if (end === false) return result
123
124   //                              ' "key"  : "value"  ,'
125   // skipping value                          ^^^^^^^
126   while(tokens[end].stack.length > level) end--
127
128   if (!is_array) {
129     while(is_whitespace(tokens[end].type)) {
130       if (end < begin) return result
131       if (tokens[end].type === 'whitespace') {
132         result.sep2.unshift(tokens[end])
133       } else {
134         // newline, comment or other unrecognized codestyle
135         return result
136       }
137       end--
138     }
139
140     //                              ' "key"  : "value"  ,'
141     // skipping separator                    ^
142     assert.equal(tokens[end].type, 'separator')
143     assert.equal(tokens[end].raw, ':')
144     while(is_whitespace(tokens[--end].type)) {
145       if (end < begin) return result
146       if (tokens[end].type === 'whitespace') {
147         result.sep1.unshift(tokens[end])
148       } else {
149         // newline, comment or other unrecognized codestyle
150         return result
151       }
152     }
153
154     assert.equal(tokens[end].type, 'key')
155     end--
156   }
157
158   //                              ' "key"  : "value"  ,'
159   // skipping key                   ^^^^^
160   while(is_whitespace(tokens[end].type)) {
161     if (end < begin) return result
162     if (tokens[end].type === 'whitespace') {
163       result.prefix.unshift(tokens[end])
164     } else if (tokens[end].type === 'newline') {
165       result.newline.unshift(tokens[end])
166       return result
167     } else {
168       // comment or other unrecognized codestyle
169       return result
170     }
171     end--
172   }
173
174   return result
175 }
176
177 function Document(text, options) {
178   var self = Object.create(Document.prototype)
179
180   if (options == null) options = {}
181   //options._structure = true
182   var tokens = self._tokens = tokenize(text, options)
183   self._data = tokens.data
184   tokens.data = null
185   self._options = options
186
187   var stats = analyze(text, options)
188   if (options.indent == null) {
189     options.indent = stats.indent
190   }
191   if (options.quote == null) {
192     options.quote = stats.quote
193   }
194   if (options.quote_keys == null) {
195     options.quote_keys = stats.quote_keys
196   }
197   if (options.no_trailing_comma == null) {
198     options.no_trailing_comma = !stats.has_trailing_comma
199   }
200   return self
201 }
202
203 // return true if it's a proper object
204 //        throw otherwise
205 function check_if_can_be_placed(key, object, is_unset) {
206   //if (object == null) return false
207   function error(add) {
208     return Error("You can't " + (is_unset ? 'unset' : 'set') + " key '" + key + "'" + add)
209   }
210
211   if (!isObject(object)) {
212     throw error(' of an non-object')
213   }
214   if (Array.isArray(object)) {
215     // array, check boundary
216     if (String(key).match(/^\d+$/)) {
217       key = Number(String(key))
218       if (object.length < key || (is_unset && object.length === key)) {
219         throw error(', out of bounds')
220       } else if (is_unset && object.length !== key+1) {
221         throw error(' in the middle of an array')
222       } else {
223         return true
224       }
225     } else {
226       throw error(' of an array')
227     }
228   } else {
229     // object
230     return true
231   }
232 }
233
234 // usage: document.set('path.to.something', 'value')
235 //    or: document.set(['path','to','something'], 'value')
236 Document.prototype.set = function(path, value) {
237   path = arg_to_path(path)
238
239   // updating this._data and check for errors
240   if (path.length === 0) {
241     if (value === undefined) throw Error("can't remove root document")
242     this._data = value
243     var new_key = false
244
245   } else {
246     var data = this._data
247
248     for (var i=0; i<path.length-1; i++) {
249       check_if_can_be_placed(path[i], data, false)
250       data = data[path[i]]
251     }
252     if (i === path.length-1) {
253       check_if_can_be_placed(path[i], data, value === undefined)
254     }
255
256     var new_key = !(path[i] in data)
257
258     if (value === undefined) {
259       if (Array.isArray(data)) {
260         data.pop()
261       } else {
262         delete data[path[i]]
263       }
264     } else {
265       data[path[i]] = value
266     }
267   }
268
269   // for inserting document
270   if (!this._tokens.length)
271     this._tokens = [ { raw: '', type: 'literal', stack: [], value: undefined } ]
272
273   var position = [
274     find_first_non_ws_token(this._tokens, 0, this._tokens.length - 1),
275     find_last_non_ws_token(this._tokens, 0, this._tokens.length - 1),
276   ]
277   for (var i=0; i<path.length-1; i++) {
278     position = find_element_in_tokenlist(path[i], i, this._tokens, position[0], position[1])
279     if (position == false) throw Error('internal error, please report this')
280   }
281   // assume that i == path.length-1 here
282
283   if (path.length === 0) {
284     var newtokens = value_to_tokenlist(value, path, this._options)
285     // all good
286
287   } else if (!new_key) {
288     // replace old value with a new one (or deleting something)
289     var pos_old = position
290     position = find_element_in_tokenlist(path[i], i, this._tokens, position[0], position[1])
291
292     if (value === undefined && position !== false) {
293       // deleting element (position !== false ensures there's something)
294       var newtokens = []
295
296       if (!Array.isArray(data)) {
297         // removing element from an object, `{x:1, key:CURRENT} -> {x:1}`
298         // removing sep, literal and optional sep
299         // ':'
300         var pos2 = find_last_non_ws_token(this._tokens, pos_old[0], position[0] - 1)
301         assert.equal(this._tokens[pos2].type, 'separator')
302         assert.equal(this._tokens[pos2].raw, ':')
303         position[0] = pos2
304
305         // key
306         var pos2 = find_last_non_ws_token(this._tokens, pos_old[0], position[0] - 1)
307         assert.equal(this._tokens[pos2].type, 'key')
308         assert.equal(this._tokens[pos2].value, path[path.length-1])
309         position[0] = pos2
310       }
311
312       // removing comma in arrays and objects
313       var pos2 = find_last_non_ws_token(this._tokens, pos_old[0], position[0] - 1)
314       assert.equal(this._tokens[pos2].type, 'separator')
315       if (this._tokens[pos2].raw === ',') {
316         position[0] = pos2
317       } else {
318         // beginning of the array/object, so we should remove trailing comma instead
319         pos2 = find_first_non_ws_token(this._tokens, position[1] + 1, pos_old[1])
320         assert.equal(this._tokens[pos2].type, 'separator')
321         if (this._tokens[pos2].raw === ',') {
322           position[1] = pos2
323         }
324       }
325
326     } else {
327       var indent = pos2 !== false
328                  ? detect_indent_style(this._tokens, Array.isArray(data), pos_old[0], position[1] - 1, i)
329                  : {}
330       var newtokens = value_to_tokenlist(value, path, this._options, false, indent)
331     }
332
333   } else {
334     // insert new key, that's tricky
335     var path_1 = path.slice(0, i)
336
337     //  find a last separator after which we're inserting it
338     var pos2 = find_last_non_ws_token(this._tokens, position[0] + 1, position[1] - 1)
339     assert(pos2 !== false)
340
341     var indent = pos2 !== false
342                ? detect_indent_style(this._tokens, Array.isArray(data), position[0] + 1, pos2, i)
343                : {}
344
345     var newtokens = value_to_tokenlist(value, path, this._options, false, indent)
346
347     // adding leading whitespaces according to detected codestyle
348     var prefix = []
349     if (indent.newline && indent.newline.length)
350       prefix = prefix.concat(indent.newline)
351     if (indent.prefix && indent.prefix.length)
352       prefix = prefix.concat(indent.prefix)
353
354     // adding '"key":' (as in "key":"value") to object values
355     if (!Array.isArray(data)) {
356       prefix = prefix.concat(value_to_tokenlist(path[path.length-1], path_1, this._options, true))
357       if (indent.sep1 && indent.sep1.length)
358         prefix = prefix.concat(indent.sep1)
359       prefix.push({raw: ':', type: 'separator', stack: path_1})
360       if (indent.sep2 && indent.sep2.length)
361         prefix = prefix.concat(indent.sep2)
362     }
363
364     newtokens.unshift.apply(newtokens, prefix)
365
366     // check if prev token is a separator AND they're at the same level
367     if (this._tokens[pos2].type === 'separator' && this._tokens[pos2].stack.length === path.length-1) {
368       // previous token is either , or [ or {
369       if (this._tokens[pos2].raw === ',') {
370         // restore ending comma
371         newtokens.push({raw: ',', type: 'separator', stack: path_1})
372       }
373     } else {
374       // previous token isn't a separator, so need to insert one
375       newtokens.unshift({raw: ',', type: 'separator', stack: path_1})
376     }
377
378     if (indent.suffix && indent.suffix.length)
379       newtokens.push.apply(newtokens, indent.suffix)
380
381     assert.equal(this._tokens[position[1]].type, 'separator')
382     position[0] = pos2+1
383     position[1] = pos2
384   }
385
386   newtokens.unshift(position[1] - position[0] + 1)
387   newtokens.unshift(position[0])
388   this._tokens.splice.apply(this._tokens, newtokens)
389
390   return this
391 }
392
393 // convenience method
394 Document.prototype.unset = function(path) {
395   return this.set(path, undefined)
396 }
397
398 Document.prototype.get = function(path) {
399   path = arg_to_path(path)
400
401   var data = this._data
402   for (var i=0; i<path.length; i++) {
403     if (!isObject(data)) return undefined
404     data = data[path[i]]
405   }
406   return data
407 }
408
409 Document.prototype.has = function(path) {
410   path = arg_to_path(path)
411
412   var data = this._data
413   for (var i=0; i<path.length; i++) {
414     if (!isObject(data)) return false
415     data = data[path[i]]
416   }
417   return data !== undefined
418 }
419
420 // compare old object and new one, and change differences only
421 Document.prototype.update = function(value) {
422   var self = this
423   change([], self._data, value)
424   return self
425
426   function change(path, old_data, new_data) {
427     if (!isObject(new_data) || !isObject(old_data)) {
428       // if source or dest is primitive, just replace
429       if (new_data !== old_data)
430         self.set(path, new_data)
431
432     } else if (Array.isArray(new_data) != Array.isArray(old_data)) {
433       // old data is an array XOR new data is an array, replace as well
434       self.set(path, new_data)
435
436     } else if (Array.isArray(new_data)) {
437       // both values are arrays here
438
439       if (new_data.length > old_data.length) {
440         // adding new elements, so going forward
441         for (var i=0; i<new_data.length; i++) {
442           path.push(String(i))
443           change(path, old_data[i], new_data[i])
444           path.pop()
445         }
446
447       } else {
448         // removing something, so going backward
449         for (var i=old_data.length-1; i>=0; i--) {
450           path.push(String(i))
451           change(path, old_data[i], new_data[i])
452           path.pop()
453         }
454       }
455
456     } else {
457       // both values are objects here
458       for (var i in new_data) {
459         path.push(String(i))
460         change(path, old_data[i], new_data[i])
461         path.pop()
462       }
463
464       for (var i in old_data) {
465         if (i in new_data) continue
466         path.push(String(i))
467         change(path, old_data[i], new_data[i])
468         path.pop()
469       }
470     }
471   }
472 }
473
474 Document.prototype.toString = function() {
475   return this._tokens.map(function(x) {
476     return x.raw
477   }).join('')
478 }
479
480 module.exports.Document = Document
481
482 module.exports.update = function updateJSON(source, new_value, options) {
483   return Document(source, options).update(new_value).toString()
484 }
485