gun/lib/ison.js

704 lines
19 KiB
JavaScript

/* **************************************************************************
* A modified version of yieldable-json package that's backwards compatible
* with GunDB's YSON implementation
*
* (c) Copyright IBM Corp. 2017
*
* This program and the accompanying materials are made available
* under the terms of the Apache License v2.0 which accompanies
* this distribution.
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* Contributors:
* Multiple authors (IBM Corp.) - initial implementation and documentation
* **************************************************************************/
;(function () {
var yson = {}, u;
let counter = 0;
let objStack = [];
let temp = '';
const limit = 100000;
function StringifyError(m) {
this.name = 'Error';
this.message = m;
}
/**
* Checking for unicode and backslash characters and replaces if any.
* @param { string }
* @return { string }
*/
let normalize = (string, flagN) => {
let retStr = '';
let transform = '';
let uc =
'/[\\\'\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4' +
'\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g';
let unicode = new RegExp(uc);
// Taking '\\' out of the loop to avoid change in
// order of execution of object entries resulting
// in unwanted side effect
string = string.replace(/\\/gi, '\\\\');
let escape = {
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"': '\\"',
};
// Escape is implemented globally
for(var pattern in escape) {
var regex = new RegExp(pattern,'gi')
string = string.replace(regex, escape[pattern])
}
unicode.lastIndex = 0;
if (unicode.test(string)) {
// Unicode logic here
transform = string.replace(unicode, (a) => {
return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
});
if (flagN === 1) {
transform += temp;
transform += transform;
temp = '';
return '"' + transform + '"';
} else if (flagN === 2) {
return '"' + transform + '"';
} else {
temp += transform;
}
} else {
if (flagN === 1) {
retStr += temp;
retStr += string;
temp = '';
return '"' + retStr + '"';
} else if (flagN === 2) {
return '"' + string + '"';
} else {
temp += string;
return;
}
}
};
/**
* Obtain stringified value by yielding at required intensity
* @param { string} field
* @param { primitive data type } container
* @param { function or array } replacer
* @param { number or string } space
* @param { number } intensity
* @return { function } yieldCPU
*/
function * stringifyYield(field, container, replacer, space, intensity) {
let itr = 0;
let key = '';
let val = '';
let length = 0;
let tempVal = '';
let result = '';
let value = container[field];
// Made scope local handling async issues
let flag1 = 0;
let returnStr = '';
let subStr = '';
let len = 0;
// Yield the stringification at definite intervals
if (++counter > 512 * intensity) {
counter = 0;
yield val;
}
// Call replacer if one is present (SPEC)
if (typeof replacer === 'function') {
value = replacer.call(container, field, value);
}
switch (typeof value) {
case 'string':
if (value.length > limit) {
for (let l = 0; l < value.length; l += limit) {
flag1 = 0;
yield value;
subStr = value.substr(l, limit);
len += subStr.length;
if (len === value.length)
flag1 = 1;
returnStr = normalize(subStr, flag1);
}
} else
returnStr = normalize(value, 2);
return returnStr;
case 'number':
return isFinite(value)
? String(value)
: 'null';
case 'boolean':
case 'null':
return String(value);
case 'undefined':
return;
case 'function':
return 'null';
case 'object':
if (!value)
return 'null';
// Manage special cases of Arrays and Objects
let getResult = (decision) => {
if (result.length === 0)
if (decision)
return '{}';
else
return '[]';
else
if (decision)
if (space)
return '{\n' + space + result.join(',\n' + space) + '\n' + '}';
else
return '{' + result.join(',') + '}';
else
if (space)
return '[\n' + space + result.join(',\n' + space) + '\n' + ']';
else
return '[' + result.join(',') + ']';
};
result = [];
// If toJSON is present, invoke it (SPEC)
if (value && typeof value.toJSON === 'function') {
const response = value.toJSON(field);
if (response === undefined) {
return undefined;
}
if (typeof response === "number") {
result.push(value.toJSON(field));
} else {
result.push('"' + value.toJSON(field) + '"');
}
if (result.length === 0)
return '{}';
else
if (space)
return space + result.join(',\n' + space) + '\n';
else
return result.join(',');
}
// Array case
if (value && value.constructor === Array) {
length = value.length;
for (itr = 0; itr < length; itr += 1) {
tempVal =
yield *stringifyYield(itr, value, replacer, space, intensity) ||
'null';
if (tempVal !== undefined)
result.push(tempVal);
}
return getResult(false);
}
// Manage replacing object scenario (SPEC)
if (replacer && typeof replacer === 'object') {
length = replacer.length;
for (itr = 0; itr < length; itr += 1) {
if (typeof replacer[itr] === 'string') {
key = replacer[itr];
val = yield *stringifyYield(key, value, replacer, space, intensity);
if (val !== undefined)
result.push(normalize(key, 2) + (space
? ': '
: ':') + val);
}
}
} else {
// Object case
objStack.push(value);
for (key in value) {
if (typeof value[key] === 'object' && value[key] !== null &&
value[key] !== undefined) {
if (objStack.indexOf(value[key]) !== -1) {
return new StringifyError('Circular Structure Detected');
} else
objStack.push(value[key]);
}
if (Object.hasOwnProperty.call(value, key)) {
val = yield *stringifyYield(key, value, replacer, space, intensity);
if (val !== undefined)
result.push(normalize(key, 2) + (space
? ': '
: ':') + val);
}
objStack = objStack.filter((v, i, a) => { return v !== value[key] });
}
objStack = objStack.filter((v, i, a) => { return v !== value });
}
return getResult(true);
default:
return new StringifyError('Unexpected Character');
}
}
/**
* Calling appropriate functions each time.
* @param { primitive data types } value
* @param { function or array } replacer
* @param { number or string } space
* @param { number } intensity
* @param { function } callback
* @return { function } yieldCPU
*/
let stringifyWrapper = (value, replacer, space, intensity, callback) => {
let indent = '';
if (typeof space === 'number') {
indent = ' '.repeat(space);
} else if (typeof space === 'string') {
indent = space;
}
let yielding;
// To hold 'stringifyYield' genarator function
function * yieldBridge() {
yielding = yield *stringifyYield('', {'': value}, replacer, indent, 1);
}
let rs = yieldBridge();
let g = rs.next();
let yieldCPU = () => {
setTimeout(() => {
g = rs.next();
if (g && g.done === true) {
// Reinitializing the values at the end of API call
counter = 0;
temp = ''
objStack = [];
if (typeof yielding === 'object')
return callback(yielding, null);
else
return callback(null, yielding);
}
yieldCPU();
}, 0);
};
return yieldCPU();
};
/**
* This method parses a JSON text to produce an object or array.
* It can throw a SyntaxError exception, if the string is malformed.
* @param { string } text
* @param { function or array } reviver
* @param { number } intensity
* @param { function } cb
* @return { function } yieldCPU
*/
let parseWrapper = (text, reviver, intensity, cb) => {
let counter = 0;
let keyN = 0;
let parseStr = text;
let at = 0;
let ch = ' ';
let word = '';
function ParseError(m) {
this.name = 'ParseError';
this.message = m;
this.text = parseStr;
}
// Seek to the next character, after skipping white spaces, if any.
let seek = () => {
ch = parseStr.charAt && parseStr.charAt(at);
at++;
while (ch && ch <= ' ') {
seek();
}
return ch;
};
// Seek to the previous character, required in some special cases.
let unseek = () => {
ch = parseStr.charAt(--at);
};
// Match 'true', 'false' and 'null' built-ins.
let wordCheck = () => {
word = '';
do {
word += ch;
seek();
} while (ch.match(/[a-z]/i));
parseStr = parseStr.slice(at - 1);
at = 0;
return word;
};
// Process strings specially.
let normalizeUnicodedString = () => {
let inQuotes = ' ';
let tempIndex = at;
let index = 0;
let slash = 0;
let c = '"';
while (c) {
index = parseStr.indexOf('"', tempIndex + 1);
tempIndex = index;
ch = parseStr.charAt(tempIndex - 1);
while (ch === '\\') {
slash++;
ch = parseStr.charAt(tempIndex - (slash + 1));
}
if (slash % 2 === 0) {
inQuotes = parseStr.substring(at, index);
parseStr = parseStr.slice(++index);
slash = 0;
break;
} else
slash = 0;
}
// When parsing string values, look for " and \ characters.
index = inQuotes.indexOf('\\');
while (index >= 0) {
let escapee = {
'"': '"',
'\'': '\'',
'/': '/',
'\\': '\\',
b: '\b',
f: '\f',
n: '\n',
r: '\r',
t: '\t',
};
let hex = 0;
let i = 0;
let uffff = 0;
at = index;
ch = inQuotes.charAt(++at);
if (ch === 'u') {
uffff = 0;
for (i = 0; i < 4; i += 1) {
hex = parseInt(ch = inQuotes.charAt(++at), 16);
if (!isFinite(hex)) {
break;
}
uffff = uffff * 16 + hex;
}
inQuotes = inQuotes.slice(0, index) +
String.fromCharCode(uffff) + inQuotes.slice(index + 6);
at = index;
} else if (typeof escapee[ch] === 'string') {
inQuotes = inQuotes.slice(0, index) +
escapee[ch] + inQuotes.slice(index + 2);
at = index + 1;
} else
break;
index = inQuotes.indexOf('\\', at);
}
at = 0;
return inQuotes;
};
/**
* This function parses the current string and returns the JavaScript
* Object, through recursive method, and yielding back occasionally
* based on the intensity parameter.
* @return { object } returnObj
*/
function * parseYield() {
let key = '';
let returnObj = {};
let returnArr = [];
let v = '';
let inQuotes = '';
let num = 0;
let numHolder = '';
let addup = () => {
numHolder += ch;
seek();
};
// Handle premitive types. eg: JSON.parse(21)
if (typeof parseStr === 'number' || typeof parseStr === 'boolean' || typeof parseStr === "function" ||
parseStr === null) {
parseStr = '';
return text;
} else if (typeof parseStr === 'undefined') {
parseStr = undefined;
return text;
} else if (parseStr.charAt && parseStr.charAt(0) === '[' && parseStr.charAt(1) === ']') {
parseStr = '';
return [];
} else if (parseStr.charAt && parseStr.charAt(0) === '{' && parseStr.charAt(1) === '}') {
parseStr = '';
return {};
} else {
// Yield the parsing work at specified intervals.
if (++counter > 512 * intensity) {
counter = 0;
yield;
}
// Common case: non-premitive types.
if (keyN !== 1)
seek();
switch (ch) {
case '{':
// Object case
seek();
if (ch === '}') {
parseStr = parseStr.slice(at);
at = 0;
return returnObj;
}
do {
if (ch !== '"')
seek();
keyN = 1;
key = yield *parseYield();
keyN = 0;
seek();
returnObj[key] = yield *parseYield();
seek();
if (ch === '}') {
parseStr = parseStr.slice(at);
at = 0;
return returnObj;
}
} while (ch === ',');
return new ParseError('Bad object');
case '[':
// Array case
seek();
if (ch === ']') {
parseStr = parseStr.slice(at);
at = 0;
return returnArr;
}
unseek();
do {
v = yield *parseYield();
returnArr.push(v);
seek();
if (ch === ']') {
parseStr = parseStr.slice(at);
at = 0;
return returnArr;
}
} while (ch === ',');
return new ParseError('Bad array');
case '"':
parseStr = parseStr.slice(at - 1);
at = 0;
if (parseStr.charAt(0) === '"' && parseStr.charAt(1) === '"') {
parseStr = parseStr.slice(2);
at = 0;
return inQuotes;
} else {
seek();
return normalizeUnicodedString();
}
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case '-':
if (ch === '-') addup();
do {
addup();
if (ch === '.' || ch === 'e' || ch === 'E' ||
ch === '-' || ch === '+' ||
(ch >= String.fromCharCode(65) &&
ch <= String.fromCharCode(70)))
addup();
} while (ch === '-' || ch === '+' || (isFinite(ch) && ch !== ''));
num = Number(numHolder);
parseStr = parseStr.slice(at - 1);
at = 0;
return num;
case 't':
word = wordCheck();
if (word === 'true')
return true;
else return new ParseError('Unexpected character');
case 'f':
word = wordCheck();
if (word === 'false')
return false;
else return new ParseError('Unexpected character');
case 'n':
word = wordCheck();
if (word === 'null')
return null;
else return new ParseError('Unexpected character');
default:
return new ParseError('Unexpected character');
}
}
}
/**
* If there is a reviver function, we recursively walk the new structure,
* passing each name/value pair to the reviver function for possible
* transformation, starting with a temporary root object that holds the result
* in an empty key. If there is not a reviver function, we simply return the
* result.
* @param { object } yieldedObject
* @param { string } key
* @return { function } reviver
*/
let revive = (yieldedObject, key) => {
let k = '';
let v = '';
let val = yieldedObject[key];
if (val && typeof val === 'object') {
for (k in val) {
if (Object.prototype.hasOwnProperty.call(val, k)) {
v = revive(val, k);
if (v !== undefined)
val[k] = v;
else
delete val[k];
}
}
}
return reviver.call(yieldedObject, key, val);
};
let yielding = '';
// To hold 'parseYield' genarator function
function * yieldBridge() {
yielding = yield* parseYield();
}
let rs = yieldBridge();
let gen = rs.next();
// Main yield control logic.
let yieldCPU = () => {
setTimeout(() => {
gen = rs.next();
if (gen && gen.done === true) {
let isEmpty = (value) => {
if (value.charAt(0) === '}' || value.charAt(0) === ']')
value = value.substring(1, value.length);
return typeof value === 'string' && !value.trim();
};
if (typeof yielding === 'undefined')
return cb(new ParseError('Unexpected Character'), null);
else if (yielding instanceof ParseError)
return cb(yielding, null);
else if (!isEmpty(parseStr))
return cb(new ParseError('Unexpected Character'), null);
else {
if (reviver !== null) {
if (typeof reviver === 'function') {
let result = revive({'': yielding}, '');
return cb(null, result);
}
} else
return cb(null, yielding);
}
}
yieldCPU();
}), 0;
};
return yieldCPU();
};
/**
* Checks whether the provided space
* @param { string or number } space
* @return { string or number }
*/
let validateSpace = (space) => {
if (typeof space === 'number') {
space = Math.round(space);
if (space >= 1 && space <= 10)
return space;
else if (space < 1)
return 0;
else
return 10;
} else {
if (space.length <= 10)
return space;
else
return space.substr(0, 9);
}
};
/**
* Checks whether the provided intensity
* @param { number } intensity
* @return { number }
*/
let validateIntensity = (intensity) => {
intensity = Math.round(intensity);
if (intensity > 0 && intensity <= 32)
return intensity;
else if (intensity <= 0)
return 1;
else
return 32;
};
yson.parseAsync = function (data, callback, reviver = null, intensity = 1) {
//Bring parity with the in-built parser, that takes both string and buffer
if (Buffer.isBuffer(data))
data = data.toString();
if (!callback)
throw new Error('Missing Callback');
intensity = validateIntensity(intensity);
return parseWrapper(data, reviver, intensity, callback);
};
/**
* Error checking and call of appropriate functions for JSON stringify API
* @param { primitive data types } data
* @param { function or array } replacer
* @param { number or string } space
* @param { number } intensity
* @param { function } callback
* @return { function } stringifyWrapper
*/
yson.stringifyAsync = function(data, callback, replacer = null, space, intensity = 1) {
if (typeof callback !== 'function') {
throw new TypeError('Callback is not a function');
}
if (typeof space === 'number' || typeof space === 'string')
space = validateSpace(space);
if (typeof intensity === 'number')
intensity = validateIntensity(intensity);
return stringifyWrapper(data, replacer, space, intensity, callback);
}
if(typeof window != ''+u){ window.YSON = yson }
try{ if(typeof module != ''+u){ module.exports = yson } }catch(e){}
if(typeof JSON != ''+u){
JSON.parseAsync = yson.parseAsync;
JSON.stringifyAsync = yson.stringifyAsync;
}
}());