579 lines
13 KiB
JavaScript
579 lines
13 KiB
JavaScript
/*
|
|
Copyright (c) 2013, Rodrigo González, Sapienlab All Rights Reserved.
|
|
Available via MIT LICENSE. See https://github.com/roro89/jsonpack/blob/master/LICENSE.md for details.
|
|
*/
|
|
(function(define) {
|
|
|
|
define([], function() {
|
|
|
|
var TOKEN_TRUE = -1;
|
|
var TOKEN_FALSE = -2;
|
|
var TOKEN_NULL = -3;
|
|
var TOKEN_EMPTY_STRING = -4;
|
|
var TOKEN_UNDEFINED = -5;
|
|
|
|
var pack = function(json, options) {
|
|
|
|
// Canonizes the options
|
|
options = options || {};
|
|
|
|
// A shorthand for debugging
|
|
var verbose = options.verbose || false;
|
|
|
|
verbose && console.log('Normalize the JSON Object');
|
|
|
|
// JSON as Javascript Object (Not string representation)
|
|
json = typeof json === 'string' ? this.JSON.parse(json) : json;
|
|
|
|
verbose && console.log('Creating a empty dictionary');
|
|
|
|
// The dictionary
|
|
var dictionary = {
|
|
strings : [],
|
|
integers : [],
|
|
floats : []
|
|
};
|
|
|
|
verbose && console.log('Creating the AST');
|
|
|
|
// The AST
|
|
var ast = (function recursiveAstBuilder(item) {
|
|
|
|
verbose && console.log('Calling recursiveAstBuilder with ' + this.JSON.stringify(item));
|
|
|
|
// The type of the item
|
|
var type = typeof item;
|
|
|
|
// Case 7: The item is null
|
|
if (item === null) {
|
|
return {
|
|
type : 'null',
|
|
index : TOKEN_NULL
|
|
};
|
|
}
|
|
|
|
//add undefined
|
|
if (typeof item === 'undefined') {
|
|
return {
|
|
type : 'undefined',
|
|
index : TOKEN_UNDEFINED
|
|
};
|
|
}
|
|
|
|
// Case 1: The item is Array Object
|
|
if ( item instanceof Array) {
|
|
|
|
// Create a new sub-AST of type Array (@)
|
|
var ast = ['@'];
|
|
|
|
// Add each items
|
|
for (var i in item) {
|
|
|
|
if (!item.hasOwnProperty(i)) continue;
|
|
|
|
ast.push(recursiveAstBuilder(item[i]));
|
|
}
|
|
|
|
// And return
|
|
return ast;
|
|
|
|
}
|
|
|
|
// Case 2: The item is Object
|
|
if (type === 'object') {
|
|
|
|
// Create a new sub-AST of type Object ($)
|
|
var ast = ['$'];
|
|
|
|
// Add each items
|
|
for (var key in item) {
|
|
|
|
if (!item.hasOwnProperty(key))
|
|
continue;
|
|
|
|
ast.push(recursiveAstBuilder(key));
|
|
ast.push(recursiveAstBuilder(item[key]));
|
|
}
|
|
|
|
// And return
|
|
return ast;
|
|
|
|
}
|
|
|
|
// Case 3: The item empty string
|
|
if (item === '') {
|
|
return {
|
|
type : 'empty',
|
|
index : TOKEN_EMPTY_STRING
|
|
};
|
|
}
|
|
|
|
// Case 4: The item is String
|
|
if (type === 'string') {
|
|
|
|
// The index of that word in the dictionary
|
|
var index = _indexOf.call(dictionary.strings, item);
|
|
|
|
// If not, add to the dictionary and actualize the index
|
|
if (index == -1) {
|
|
dictionary.strings.push(_encode(item));
|
|
index = dictionary.strings.length - 1;
|
|
}
|
|
|
|
// Return the token
|
|
return {
|
|
type : 'strings',
|
|
index : index
|
|
};
|
|
}
|
|
|
|
// Case 5: The item is integer
|
|
if (type === 'number' && item % 1 === 0) {
|
|
|
|
// The index of that number in the dictionary
|
|
var index = _indexOf.call(dictionary.integers, item);
|
|
|
|
// If not, add to the dictionary and actualize the index
|
|
if (index == -1) {
|
|
dictionary.integers.push(_base10To36(item));
|
|
index = dictionary.integers.length - 1;
|
|
}
|
|
|
|
// Return the token
|
|
return {
|
|
type : 'integers',
|
|
index : index
|
|
};
|
|
}
|
|
|
|
// Case 6: The item is float
|
|
if (type === 'number') {
|
|
// The index of that number in the dictionary
|
|
var index = _indexOf.call(dictionary.floats, item);
|
|
|
|
// If not, add to the dictionary and actualize the index
|
|
if (index == -1) {
|
|
// Float not use base 36
|
|
dictionary.floats.push(item);
|
|
index = dictionary.floats.length - 1;
|
|
}
|
|
|
|
// Return the token
|
|
return {
|
|
type : 'floats',
|
|
index : index
|
|
};
|
|
}
|
|
|
|
// Case 7: The item is boolean
|
|
if (type === 'boolean') {
|
|
return {
|
|
type : 'boolean',
|
|
index : item ? TOKEN_TRUE : TOKEN_FALSE
|
|
};
|
|
}
|
|
|
|
// Default
|
|
throw new Error('Unexpected argument of type ' + typeof (item));
|
|
|
|
})(json);
|
|
|
|
// A set of shorthands proxies for the length of the dictionaries
|
|
var stringLength = dictionary.strings.length;
|
|
var integerLength = dictionary.integers.length;
|
|
var floatLength = dictionary.floats.length;
|
|
|
|
verbose && console.log('Parsing the dictionary');
|
|
|
|
// Create a raw dictionary
|
|
var packed = dictionary.strings.join('|');
|
|
packed += '^' + dictionary.integers.join('|');
|
|
packed += '^' + dictionary.floats.join('|');
|
|
|
|
verbose && console.log('Parsing the structure');
|
|
|
|
// And add the structure
|
|
packed += '^' + (function recursiveParser(item) {
|
|
|
|
verbose && console.log('Calling a recursiveParser with ' + this.JSON.stringify(item));
|
|
|
|
// If the item is Array, then is a object of
|
|
// type [object Object] or [object Array]
|
|
if ( item instanceof Array) {
|
|
|
|
// The packed resulting
|
|
var packed = item.shift();
|
|
|
|
for (var i in item) {
|
|
|
|
if (!item.hasOwnProperty(i))
|
|
continue;
|
|
|
|
packed += recursiveParser(item[i]) + '|';
|
|
}
|
|
|
|
return (packed[packed.length - 1] === '|' ? packed.slice(0, -1) : packed) + ']';
|
|
|
|
}
|
|
|
|
// A shorthand proxies
|
|
var type = item.type, index = item.index;
|
|
|
|
if (type === 'strings') {
|
|
// Just return the base 36 of index
|
|
return _base10To36(index);
|
|
}
|
|
|
|
if (type === 'integers') {
|
|
// Return a base 36 of index plus stringLength offset
|
|
return _base10To36(stringLength + index);
|
|
}
|
|
|
|
if (type === 'floats') {
|
|
// Return a base 36 of index plus stringLength and integerLength offset
|
|
return _base10To36(stringLength + integerLength + index);
|
|
}
|
|
|
|
if (type === 'boolean') {
|
|
return item.index;
|
|
}
|
|
|
|
if (type === 'null') {
|
|
return TOKEN_NULL;
|
|
}
|
|
|
|
if (type === 'undefined') {
|
|
return TOKEN_UNDEFINED;
|
|
}
|
|
|
|
if (type === 'empty') {
|
|
return TOKEN_EMPTY_STRING;
|
|
}
|
|
|
|
throw new TypeError('The item is alien!');
|
|
|
|
})(ast);
|
|
|
|
verbose && console.log('Ending parser');
|
|
|
|
// If debug, return a internal representation of dictionary and stuff
|
|
if (options.debug)
|
|
return {
|
|
dictionary : dictionary,
|
|
ast : ast,
|
|
packed : packed
|
|
};
|
|
|
|
return packed;
|
|
|
|
};
|
|
|
|
var unpack = function(packed, options) {
|
|
|
|
// Canonizes the options
|
|
options = options || {};
|
|
|
|
// A raw buffer
|
|
var rawBuffers = packed.split('^');
|
|
|
|
// Create a dictionary
|
|
options.verbose && console.log('Building dictionary');
|
|
var dictionary = [];
|
|
|
|
// Add the strings values
|
|
var buffer = rawBuffers[0];
|
|
if (buffer !== '') {
|
|
buffer = buffer.split('|');
|
|
options.verbose && console.log('Parse the strings dictionary');
|
|
for (var i=0, n=buffer.length; i<n; i++){
|
|
dictionary.push(_decode(buffer[i]));
|
|
}
|
|
}
|
|
|
|
// Add the integers values
|
|
buffer = rawBuffers[1];
|
|
if (buffer !== '') {
|
|
buffer = buffer.split('|');
|
|
options.verbose && console.log('Parse the integers dictionary');
|
|
for (var i=0, n=buffer.length; i<n; i++){
|
|
dictionary.push(_base36To10(buffer[i]));
|
|
}
|
|
}
|
|
|
|
// Add the floats values
|
|
buffer = rawBuffers[2];
|
|
if (buffer !== '') {
|
|
buffer = buffer.split('|')
|
|
options.verbose && console.log('Parse the floats dictionary');
|
|
for (var i=0, n=buffer.length; i<n; i++){
|
|
dictionary.push(parseFloat(buffer[i]));
|
|
}
|
|
}
|
|
// Free memory
|
|
buffer = null;
|
|
|
|
options.verbose && console.log('Tokenizing the structure');
|
|
|
|
// Tokenizer the structure
|
|
var number36 = '';
|
|
var tokens = [];
|
|
var len=rawBuffers[3].length;
|
|
for (var i = 0; i < len; i++) {
|
|
var symbol = rawBuffers[3].charAt(i);
|
|
if (symbol === '|' || symbol === '$' || symbol === '@' || symbol === ']') {
|
|
if (number36) {
|
|
tokens.push(_base36To10(number36));
|
|
number36 = '';
|
|
}
|
|
symbol !== '|' && tokens.push(symbol);
|
|
} else {
|
|
number36 += symbol;
|
|
}
|
|
}
|
|
|
|
// A shorthand proxy for tokens.length
|
|
var tokensLength = tokens.length;
|
|
|
|
// The index of the next token to read
|
|
var tokensIndex = 0;
|
|
|
|
options.verbose && console.log('Starting recursive parser');
|
|
|
|
return (function recursiveUnpackerParser() {
|
|
|
|
// Maybe '$' (object) or '@' (array)
|
|
var type = tokens[tokensIndex++];
|
|
|
|
options.verbose && console.log('Reading collection type ' + (type === '$' ? 'object' : 'Array'));
|
|
|
|
// Parse an array
|
|
if (type === '@') {
|
|
|
|
var node = [];
|
|
|
|
for (; tokensIndex < tokensLength; tokensIndex++) {
|
|
var value = tokens[tokensIndex];
|
|
options.verbose && console.log('Read ' + value + ' symbol');
|
|
if (value === ']')
|
|
return node;
|
|
if (value === '@' || value === '$') {
|
|
node.push(recursiveUnpackerParser());
|
|
} else {
|
|
switch(value) {
|
|
case TOKEN_TRUE:
|
|
node.push(true);
|
|
break;
|
|
case TOKEN_FALSE:
|
|
node.push(false);
|
|
break;
|
|
case TOKEN_NULL:
|
|
node.push(null);
|
|
break;
|
|
case TOKEN_UNDEFINED:
|
|
node.push(undefined);
|
|
break;
|
|
case TOKEN_EMPTY_STRING:
|
|
node.push('');
|
|
break;
|
|
default:
|
|
node.push(dictionary[value]);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
options.verbose && console.log('Parsed ' + this.JSON.stringify(node));
|
|
|
|
return node;
|
|
|
|
}
|
|
|
|
// Parse a object
|
|
if (type === '$') {
|
|
var node = {};
|
|
|
|
for (; tokensIndex < tokensLength; tokensIndex++) {
|
|
|
|
var key = tokens[tokensIndex];
|
|
|
|
if (key === ']')
|
|
return node;
|
|
|
|
if (key === TOKEN_EMPTY_STRING)
|
|
key = '';
|
|
else
|
|
key = dictionary[key];
|
|
|
|
var value = tokens[++tokensIndex];
|
|
|
|
if (value === '@' || value === '$') {
|
|
node[key] = recursiveUnpackerParser();
|
|
} else {
|
|
switch(value) {
|
|
case TOKEN_TRUE:
|
|
node[key] = true;
|
|
break;
|
|
case TOKEN_FALSE:
|
|
node[key] = false;
|
|
break;
|
|
case TOKEN_NULL:
|
|
node[key] = null;
|
|
break;
|
|
case TOKEN_UNDEFINED:
|
|
node[key] = undefined;
|
|
break;
|
|
case TOKEN_EMPTY_STRING:
|
|
node[key] = '';
|
|
break;
|
|
default:
|
|
node[key] = dictionary[value];
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
options.verbose && console.log('Parsed ' + this.JSON.stringify(node));
|
|
|
|
return node;
|
|
}
|
|
|
|
throw new TypeError('Bad token ' + type + ' isn\'t a type');
|
|
|
|
})();
|
|
|
|
}
|
|
/**
|
|
* Get the index value of the dictionary
|
|
* @param {Object} dictionary a object that have two array attributes: 'string' and 'number'
|
|
* @param {Object} data
|
|
*/
|
|
var _indexOfDictionary = function(dictionary, value) {
|
|
|
|
// The type of the value
|
|
var type = typeof value;
|
|
|
|
// If is boolean, return a boolean token
|
|
if (type === 'boolean')
|
|
return value ? TOKEN_TRUE : TOKEN_FALSE;
|
|
|
|
// If is null, return a... yes! the null token
|
|
if (value === null)
|
|
return TOKEN_NULL;
|
|
|
|
//add undefined
|
|
if (typeof value === 'undefined')
|
|
return TOKEN_UNDEFINED;
|
|
|
|
|
|
if (value === '') {
|
|
return TOKEN_EMPTY_STRING;
|
|
}
|
|
|
|
if (type === 'string') {
|
|
value = _encode(value);
|
|
var index = _indexOf.call(dictionary.strings, value);
|
|
if (index === -1) {
|
|
dictionary.strings.push(value);
|
|
index = dictionary.strings.length - 1;
|
|
}
|
|
}
|
|
|
|
// If has an invalid JSON type (example a function)
|
|
if (type !== 'string' && type !== 'number') {
|
|
throw new Error('The type is not a JSON type');
|
|
};
|
|
|
|
if (type === 'string') {// string
|
|
value = _encode(value);
|
|
} else if (value % 1 === 0) {// integer
|
|
value = _base10To36(value);
|
|
} else {// float
|
|
|
|
}
|
|
|
|
// If is number, "serialize" the value
|
|
value = type === 'number' ? _base10To36(value) : _encode(value);
|
|
|
|
// Retrieve the index of that value in the dictionary
|
|
var index = _indexOf.call(dictionary[type], value);
|
|
|
|
// If that value is not in the dictionary
|
|
if (index === -1) {
|
|
// Push the value
|
|
dictionary[type].push(value);
|
|
// And return their index
|
|
index = dictionary[type].length - 1;
|
|
}
|
|
|
|
// If the type is a number, then add the '+' prefix character
|
|
// to differentiate that they is a number index. If not, then
|
|
// just return a 36-based representation of the index
|
|
return type === 'number' ? '+' + index : index;
|
|
|
|
};
|
|
|
|
var _encode = function(str) {
|
|
if ( typeof str !== 'string')
|
|
return str;
|
|
|
|
return str.replace(/[\+ \|\^\%]/g, function(a) {
|
|
return ({
|
|
' ' : '+',
|
|
'+' : '%2B',
|
|
'|' : '%7C',
|
|
'^' : '%5E',
|
|
'%' : '%25'
|
|
})[a]
|
|
});
|
|
};
|
|
|
|
var _decode = function(str) {
|
|
if ( typeof str !== 'string')
|
|
return str;
|
|
|
|
return str.replace(/\+|%2B|%7C|%5E|%25/g, function(a) {
|
|
return ({
|
|
'+' : ' ',
|
|
'%2B' : '+',
|
|
'%7C' : '|',
|
|
'%5E' : '^',
|
|
'%25' : '%'
|
|
})[a]
|
|
})
|
|
};
|
|
|
|
var _base10To36 = function(number) {
|
|
return Number.prototype.toString.call(number, 36).toUpperCase();
|
|
};
|
|
|
|
var _base36To10 = function(number) {
|
|
return parseInt(number, 36);
|
|
};
|
|
|
|
var _indexOf = Array.prototype.indexOf ||
|
|
function(obj, start) {
|
|
for (var i = (start || 0), j = this.length; i < j; i++) {
|
|
if (this[i] === obj) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
};
|
|
|
|
return {
|
|
JSON : JSON,
|
|
pack : pack,
|
|
unpack : unpack
|
|
};
|
|
|
|
});
|
|
|
|
})( typeof define == 'undefined' || !define.amd ? function(deps, factory) {
|
|
var jsonpack = factory();
|
|
if ( typeof exports != 'undefined')
|
|
for (var key in jsonpack)
|
|
exports[key] = jsonpack[key];
|
|
else
|
|
window.jsonpack = jsonpack;
|
|
} : define);
|