niosh-json
Version:
extended json parser and stringify
441 lines (440 loc) • 25.8 kB
JavaScript
//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
//#region –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––-–––––––––––––––-––––––––––––––– define function of revivers and replacers
/**
* @callback JSONKeyValueFunction
* @param {string} key
* @param {any} value
* @return {any}
*/
//#endregion
//#region –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––-–––––––––––––––-––––––––––––––– define configuration object
/**
* Object for setup JSON
* - debug : active or disactive messages (default false)
* - circulars : enable writing of circulars properties (default false)
* - nothorws : disable exceptions on errors returning undefined (default false)
* @typedef {Object} JSONConfigurationObject
* @property {boolean} debug
* @property {boolean} circulars
* @property {boolean} nothrows
*/
//#endregion
//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
//#region –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––-–––––––––––––––-––––––––––––––– create internal secret memory area called "µ" as memory
const µ = JSON.µ = {
stringify: JSON.stringify, // backup original stringify
parse: JSON.parse, // backup original parse
// --------------------------------------------------------------------------------------------------- configuration
/** @type {boolean} */ debug: false, // enable debug logging
/** @type {boolean} */ circulars: false, // enable output of circular references (stringify)
/** @type {boolean} */ nothrows: false, // disable exceptions returning undefined
/** @type {string[]} */ replacersNames: [], // vector of replacers names
/** @type {string[]} */ replacersInfos: [], // vector of replacers infos
/** @type {number} */ replacersMaxLen: 0, // replacers max string len of key names
/** @type {string[]} */ reviversNames: [], // vector of revivers names
/** @type {string[]} */ reviversInfos: [], // vector of revivers infos
/** @type {number} */ reviversMaxLen: 0, // revivers max string len of key names
/** @type {number} */ rpAndRevMaxLen: 0, // replacers and revivers max string len of key names
/** @type {JSONKeyValueFunction[]} */ replacers: [], // vector of replacers
/** @type {JSONKeyValueFunction[]} */ revivers: [], // vector of revivers
/** @type {Map<string,function>} */ patterns: new Map(), // map of pattern used for regular expression
// --------------------------------------------------------------------------------------------------- replacers manager
replacersManager: (replacer)=>{
var cache = [];
/** @type {JSONKeyValueFunction} */
var fn = function(key,value) {
if (typeof value === "object" && value != null) {
let idx = cache.indexOf(value);
if (idx >= 0)
return (JSON.µ.circulars) ? "[Circular~"+idx+"]" : undefined;
cache.push(value);
}
for(var i=0;i<JSON.µ.replacers.length;i++) {
try {
value = JSON.µ.replacers[i](key,value);
} catch(e) {
if (JSON.µ.debug) console.error("JSON-EXCEPTION",`error on replacer id ${i} named «${JSON.µ.replacersNames[i]}»`,e);
if (JSON.µ.nothrows) return undefined;
throw e;
}
}
if (typeof replacer == "function") {
try {
value = replacer(key,value);
} catch(e) {
if (JSON.µ.debug) console.error("JSON-EXCEPTION",`error on replacer given by user code`,e);
if (JSON.µ.nothrows) return undefined;
throw e;
}
}
return value;
}
return fn;
},
// --------------------------------------------------------------------------------------------------- revivers manager
reviversManager: (reviver)=>{
/** @type {JSONKeyValueFunction} */
var fn = function(key,value) {
for(var i=0;i<JSON.µ.revivers.length;i++) {
try {
value = JSON.µ.revivers[i](key,value);
} catch(e) {
if (JSON.µ.debug) console.error("JSON-EXCEPTION",`error on reviver id ${i} named «${JSON.µ.reviversNames[i]}»`,e);
if (JSON.µ.nothrows) return undefined;
throw e;
}
}
if (typeof reviver == "function") {
try {
value = reviver(key,value);
} catch(e) {
if (JSON.µ.debug) console.error("JSON-EXCEPTION",`error on reviver given by user code`,e);
if (JSON.µ.nothrows) return undefined;
throw e;
}
}
return value;
}
return fn;
},
// --------------------------------------------------------------------------------------------------- stringify extended
stringifyExtended: function(value,replacer,space) {
try {
return JSON.µ.stringify(value,JSON.µ.replacersManager(replacer),space);
} catch(e) {
if (JSON.µ.debug) console.error("JSON-EXCEPTION",`error on stringify`,e);
if (JSON.µ.nothrows) return undefined;
throw e;
}
},
// --------------------------------------------------------------------------------------------------- parse extended
parseExtended: function(text,reviver) {
try {
return JSON.µ.parse(text,JSON.µ.reviversManager(reviver));
} catch(e) {
try {
let textWrapper = JSON.stringify({payload:text});
let objectParse = JSON.µ.parse(textWrapper,JSON.µ.reviversManager(reviver));
return objectParse.payload;
} catch {
if (JSON.µ.debug) console.error("JSON-EXCEPTION",`error on parse`,e);
if (JSON.µ.nothrows) return undefined;
throw e;
}
}
}
};
//#endregion
//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
//#region –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––-–––––––––––––––-––––––––––––––– define patterns
JSON.µ.patterns.set("DATE",()=>
/(^([0-9]{4}-(((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01]))|((0[469]|11)-(0[1-9]|[12][0-9]|30))|(02-(0[1-9]|[12][0-9]))))[Tt]([01][0-9]|2[0123]):[0-5][0-9](:[0-5][0-9](.[0-9]{1,9})?)?([Zz]|([+-])([01][0-9]|2[0123]):?[0-5][0-9])?$)|(^\s*(([0-9]{4}(-\s*|[/\s]+)(((0?[13578]|1[02])(-\s*|[/\s]+)(0?[1-9]|[12][0-9]|3[01]))|((0?[469]|11)(-\s*|[/\s]+)(0?[1-9]|[12][0-9]|30))|(0?2(-\s*|[/\s]+)(0?[1-9]|[12][0-9]))))|((((0?[13578]|1[02])(-\s*|[/\s]+)(0?[1-9]|[12][0-9]|3[01]))|((0?[469]|11)(-\s*|[/\s]+)(0?[1-9]|[12][0-9]|30))|(0?2(-\s*|[/\s]+)(0?[1-9]|[12][0-9])))(-\s*|[/\s]+)[0-9]{4}))\s*(( ([01]?[0-9]|2[0123]):\s*[0-5]?[0-9](:\s*[0-5]?[0-9](.[0-9]{1,9})?)?\s*[Zz]?\s*(([+-])([01]?[0-9]|2[0123]):?\s*[0-5]?[0-9]?)?)|( ?[Zz]\s*([+-]([01]?[0-9]|2[0123]):?\s*[0-5]?[0-9]?)?))?\s*$)/
);
JSON.µ.patterns.set("FEBRUARY29",()=>
/^\s*((?<Y1>[0-9]{4})[^0-9]*)?0?2[^0-9]*29([^0-9]*(?<Y2>[0-9]{4}))?/
);
JSON.µ.patterns.set("SYMBOL",()=>
/^\s*Symbol\((?<DATA>[\s\S]+(?=\)))\)\s*$/
);
JSON.µ.patterns.set("BIGINT",()=>
/^\s*(?<NUMBER>[0-9]+)n\s*/
);
JSON.µ.patterns.set("ERROR",()=>
/(^\s* at )|([^\S\n]*$)/gm
);
JSON.µ.patterns.set("FIXFLOAT",()=>
/(?<FIX>(?<TRIPLE>(?<DIGIT>[0-9])(\k<DIGIT>){2,})(?!.*\k<TRIPLE>)[0-9]*$)/
);
JSON.µ.patterns.set("REGEXP",()=>
/^\/(?<DEF>.+)\/(?<OPT>[gmiyusd]*)$/
);
//#endregion
//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
//#region –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––-–––––––––––––––-––––––––––––––– reviver for Date object
JSON.µ.reviversNames.push("DateObjectReviver");
JSON.µ.reviversInfos.push("convert various types of date string to date object, in other cases return original string");
JSON.µ.revivers.push((key,value)=>{
if (typeof value != "string")
return value; // skip if is not a string
let regex = JSON.µ.patterns.get("DATE")();
if (regex.exec(value)==null)
return value; // skip if not match regurar expression for Date object
// --- checking if date contains february 29
regex = JSON.µ.patterns.get("FEBRUARY29")();
let match = regex.exec(value);
if (match != null) {
// --- checking a valid year
let year = parseInt(match.groups.Y1||match.groups.Y2);
if (Number.isInteger(year) && year >= 0) {
// --- verify if have february 29 for given year
let february = new Date(Date.UTC(year,2,0,0,0,0,0));
if (february.getUTCDate() != 29) {
// --- fix february 29 with 28 when not have in year
regex = JSON.µ.patterns.get("FEBRUARY29")();
value = value.replace(regex,"$<Y1>$<Y2>-02-28");
}
}
}
// --- try to transforming string in a date object and return
try {
return new Date(value);
} catch(e) {
if (JSON.µ.debug) console.debug("JSON-EXCEPTION",e);
return value; // skip for bad conversion
}
});
//#endregion
//#region –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––-–––––––––––––––-––––––––––––––– reviver for Symbol
JSON.µ.reviversNames.push("SymbolReviver");
JSON.µ.reviversInfos.push("include symbol string decoder");
JSON.µ.revivers.push((key,value)=>{
if (typeof value != "string")
return value; // skip if is not a string
let regex = JSON.µ.patterns.get("SYMBOL")();
let match = regex.exec(value);
if (match==null)
return value; // skip if not match regurar expression for symbol
value = Symbol(match.groups.DATA);
return value;
});
//#endregion
//#region –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––-–––––––––––––––-––––––––––––––– reviver for BigInt
JSON.µ.reviversNames.push("BigIntReviver");
JSON.µ.reviversInfos.push("include bigint string decoder");
JSON.µ.revivers.push((key,value)=>{
if (typeof value != "string")
return value; // skip if is not a string
let regex = JSON.µ.patterns.get("BIGINT")();
let match = regex.exec(value);
if (match==null)
return value; // skip if not match regurar expression for bigint
value = BigInt(match.groups.NUMBER);
return value;
});
//#endregion
//#region –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––-–––––––––––––––-––––––––––––––– reviver for Error object
JSON.µ.reviversNames.push("ErrorObjectReviver");
JSON.µ.reviversInfos.push("include Error object string decoder");
JSON.µ.revivers.push((key,value)=>{
if (typeof value != "object")
return value; // skip if is not an object
if (value == null)
return value; // skip if is null object
if (Array.isArray(value))
return value; // skip if is an array
if (value instanceof Boolean || value instanceof Number || value instanceof String)
return value; // skip if is a primitive object
if (value instanceof Date || value instanceof Error)
return value; // skip if is a Date or Error object
if (typeof value.errorObject == "boolean" && value.errorObject == true) {
let errorObject = new Error(value.message);
errorObject.stack = value.errorClassName + ": " + value.message + "\n at " + value.stack.join("\n at ");
errorObject.message = value.message;
let props = Object.getOwnPropertyNames(value);
while (props.length > 0) {
let prop = props.shift();
if (prop != "errorObject" && prop != "errorClassName" && prop != "stack" && prop != "message")
errorObject[prop] = value[prop];
}
value = errorObject;
}
return value;
});
//#endregion
//#region –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––-–––––––––––––––-––––––––––––––– reviver for RegExp object
JSON.µ.reviversNames.push("RegularExpressionReviver");
JSON.µ.reviversInfos.push("include RegExp object string decoder");
JSON.µ.revivers.push((key,value)=>{
if (typeof value != "string")
return value; // skip if is not a string
let regex = JSON.µ.patterns.get("REGEXP")();
let match = regex.exec(value);
if (match==null)
return value; // skip if not match regurar expression for RegExp object
value = new RegExp(match.groups.DEF,match.groups.OPT||"");
return value;
});
//#endregion
//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
//#region –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––-–––––––––––––––-––––––––––––––– replacer for Symbol
JSON.µ.replacersNames.push("SymbolReplacer");
JSON.µ.replacersInfos.push("include symbol encoder to string");
JSON.µ.replacers.push((key,value)=>{
if (typeof value == "symbol") {
value = "Symbol(" + value.description + ")";
}
return value;
});
//#endregion
//#region –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––-–––––––––––––––-––––––––––––––– replacer for BigInt
JSON.µ.replacersNames.push("BigIntReplacer");
JSON.µ.replacersInfos.push("include bigint encoder to string");
JSON.µ.replacers.push((key,value)=>{
if (typeof value == "bigint") {
value = value.toString()+"n"
}
return value;
});
//#endregion
//#region –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––-–––––––––––––––-––––––––––––––– replacer for Error object
JSON.µ.replacersNames.push("ErrorObjectReplacer");
JSON.µ.replacersInfos.push("include Error object encoder to string");
JSON.µ.replacers.push((key,value)=>{
if (value instanceof Error) {
let errorObject = { errorObject: true };
let props = Object.getOwnPropertyNames(value);
while (props.length > 0) {
let prop = props.shift();
errorObject[prop] = value[prop];
}
if (typeof errorObject.stack == "string") {
errorObject.stack = errorObject.stack.trim().replace(JSON.µ.patterns.get("ERROR")(),"").split("\n");
errorObject.errorClassName = errorObject.stack.shift();
errorObject.errorClassName = errorObject.errorClassName.substring(0,errorObject.errorClassName.indexOf(":")).trim();
}
value = errorObject;
}
return value;
});
//#endregion
//#region –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––-–––––––––––––––-––––––––––––––– replacer for RegExp object
JSON.µ.replacersNames.push("RegularExpressionReplacer");
JSON.µ.replacersInfos.push("include RegExp object encoder to string");
JSON.µ.replacers.push((key,value)=>{
if (value instanceof RegExp)
value = value.toString();
return value;
});
//#endregion
//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
//#region –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––-––––––––––––––———–––––––––––––– finalize JSON object stringify and parse
JSON.stringify = function(value,replacer,spaces) { return JSON.µ.stringifyExtended(value,replacer,spaces); };
JSON.parse = function(text,reviver) { return JSON.µ.parseExtended(text,reviver); };
// --------------------------------------------------------------------------------------------------- calculate max strings lenghts
const JSONRefreshInformations = function() {
JSON.µ.replacersMaxLen = 0;
JSON.µ.reviversMaxLen = 0;
JSON.µ.rpAndRevMaxLen = 0;
for (var i=0;i<JSON.µ.replacersNames.length;i++) {
let len = JSON.µ.replacersNames[i].length;
if (JSON.µ.replacersMaxLen<len) JSON.µ.replacersMaxLen = len;
if (JSON.µ.rpAndRevMaxLen<len) JSON.µ.rpAndRevMaxLen = len;
}
for (var i=0;i<JSON.µ.reviversNames.length;i++) {
let len = JSON.µ.reviversNames[i].length;
if (JSON.µ.reviversMaxLen<len) JSON.µ.reviversMaxLen = len;
if (JSON.µ.rpAndRevMaxLen<len) JSON.µ.rpAndRevMaxLen = len;
}
};
JSONRefreshInformations();
// --------------------------------------------------------------------------------------------------- adding extension for manage revivers and replacers
/** @return {string[]} */ JSON.replacersKeys = function() { return [...JSON.µ.replacersNames]; };
/** @return {string[]} */ JSON.reviversKeys = function() { return [...JSON.µ.reviversNames]; };
// --------------------------------------------------------------------------------------------------- replacers informations text
/** @return {string[]} */ JSON.replacersInfos = function() {
let infos = JSON.replacersKeys();
for (var i=0;i<infos.length;i++)
infos[i]= infos[i].padEnd(JSON.µ.replacersMaxLen + 5," ") + " • " + JSON.µ.replacersInfos[i];
return infos;
};
// --------------------------------------------------------------------------------------------------- revivers informations text
/** @return {string[]} */ JSON.reviversInfos = function() {
let infos = JSON.reviversKeys();
for (var i=0;i<infos.length;i++) {
infos[i]= infos[i].padEnd(JSON.µ.reviversMaxLen + 5," ") + " • " + JSON.µ.reviversInfos[i];
}
return infos;
};
// --------------------------------------------------------------------------------------------------- replacers and revivers informations text
/** @return {string[]} */ JSON.repAndRevInfos = function() {
let infos = JSON.replacersKeys();
for (var i=0;i<infos.length;i++)
infos[i]= infos[i].padEnd(JSON.µ.rpAndRevMaxLen + 5," ") + "🧾 ⏺ replacer • " + JSON.µ.replacersInfos[i];
let adder = infos;
infos = JSON.reviversKeys();
for (var i=0;i<infos.length;i++) {
infos[i]= infos[i].padEnd(JSON.µ.rpAndRevMaxLen + 5," ") + "🧮 ⏺ reviver • " + JSON.µ.reviversInfos[i];
}
adder.push(...infos);
infos = adder;
return infos;
};
//#endregion
//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
//#region –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––-––––––––––––––———–––––––––––––– JSONex static class for extension
class JSONex {
/**
* Current additional replacers informational data
* @returns {string[]}
* */
static replacersInformations() {
return JSON.replacersInfos();
}
/**
* Current additional revivers informational data
* @returns {string[]}
* */
static reviversInformations() {
return JSON.reviversInfos();
}
/**
* Current additional replacers and revivers informational data
* @returns {string[]}
* */
static repAndRevInformations() {
return JSON.repAndRevInfos();
}
/**
* Add additional replacer function
* @param {string} key
* @param {string} info
* @param {JSONKeyValueFunction} fn
*/
static addReplacer(key,info,fn) {
if (JSON.µ.replacersNames.indexOf(key)>=0) throw new Error(`JSON-EXCEPTION: replacer «${key}» already exists`);
JSON.µ.replacersNames.push(key);
JSON.µ.replacersInfos.push(info);
JSON.µ.replacers.push(fn);
JSONRefreshInformations();
}
/**
* Add additional reviver function
* @param {string} key
* @param {string} info
* @param {JSONKeyValueFunction} fn
*/
static addReviver(key,info,fn) {
if (JSON.µ.reviversNames.indexOf(key)>=0) throw new Error(`JSON-EXCEPTION: reviver «${key}» already exists`);
JSON.µ.reviversNames.push(key);
JSON.µ.reviversInfos.push(info);
JSON.µ.revivers.push(fn);
JSONRefreshInformations();
}
/**
* Return current JSON configuration
* @returns {JSONConfigurationObject}
*/
static getConfiguration() {
return {
debug: JSON.µ.debug,
circulars: JSON.µ.circulars,
nothrows: JSON.µ.nothrows
};
}
/**
* Update current JSON configuration
* @param {JSONConfigurationObject} configuration
*/
static setConfiguration(configuration) {
if (typeof configuration == "object" && configuration != null) {
JSON.µ.debug = (typeof configuration.debug == "boolean") ? configuration.debug : JSON.µ.debug;
JSON.µ.circulars = (typeof configuration.circulars == "boolean") ? configuration.circulars : JSON.µ.circulars;
JSON.µ.nothrows = (typeof configuration.nothrows == "boolean") ? configuration.nothrows : JSON.µ.nothrows;
}
}
}
//#endregion
//#region –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––-–––––––––––––––-––––––––––––––– module exporting
require('./extensions');
module.exports = { JSONex };
//#endregion