"Fossies" - the Fresh Open Source Software Archive

Member "Atom/resources/app/apm/node_modules/jju/lib/document.js" (8 Mar 2017, 14272 Bytes) of archive /windows/misc/atom-windows.zip:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Javascript source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file.

    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