UNPKG

jsox

Version:

Java Script Object eXchange.

1,390 lines (1,336 loc) 110 kB
//"use strict"; // jsox.js // JSOX JavaScript Object eXchange. Inherits human features of comments // and extended formatting from JSON6; adds macros, big number and date // support. See README.md for details. // // This file is based off of https://github.com/JSON6/ ./lib/json6.js // which is based off of https://github.com/d3x0r/sack ./src/netlib/html5.websocket/json6_parser.c // //const util = require('util'); // debug inspect. //import util from 'util'; const _JSON=JSON; // in case someone does something like JSON=JSOX; we still need a primitive _JSON for internal stringification if( "undefined" === typeof exports ) var exports = {}; const JSOX = exports || {}; exports.JSOX = JSOX; JSOX.version = "1.2.120"; function privateizeEverything() { //const _DEBUG_LL = false; //const _DEBUG_PARSING = false; //const _DEBUG_STRINGIFY = false; //const _DEBUG_PARSING_STACK = false; //const _DEBUG_PARSING_NUMBERS = false; //const _DEBUG_PARSING_DETAILS = false; //const _DEBUG_PARSING_CONTEXT = false; //const _DEBUG_REFERENCES = false; // this tracks folling context stack when the components have not been completed. //const _DEBUG_WHITESPACE = false; const hasBigInt = (typeof BigInt === "function"); const testNonIdentifierCharacters = false; // maybe an option to enable; references otherwise unused table. const VALUE_UNDEFINED = -1 const VALUE_UNSET = 0 const VALUE_NULL = 1 const VALUE_TRUE = 2 const VALUE_FALSE = 3 const VALUE_STRING = 4 const VALUE_NUMBER = 5 const VALUE_OBJECT = 6 const VALUE_NEG_NAN = 7 const VALUE_NAN = 8 const VALUE_NEG_INFINITY = 9 const VALUE_INFINITY = 10 //const VALUE_DATE = 11 // unused yet; this is actuall a subType of VALUE_NUMBER const VALUE_EMPTY = 12 // [,] makes an array with 'empty item' const VALUE_ARRAY = 13 // // internally arrayType = -1 is a normal array // arrayType = -2 is a reference array, which, which closed is resolved to // the specified object. // arrayType = -3 is a normal array, that has already had this element pushed. const knownArrayTypeNames = ["ab","u8","cu8","s8","u16","s16","u32","s32","u64","s64","f32","f64"]; let arrayToJSOX = null; let mapToJSOX = null; const knownArrayTypes = [ArrayBuffer ,Uint8Array,Uint8ClampedArray,Int8Array ,Uint16Array,Int16Array ,Uint32Array,Int32Array ,null,null//,Uint64Array,Int64Array ,Float32Array,Float64Array]; // somehow max isn't used... it would be the NEXT available VALUE_XXX value... //const VALUE_ARRAY_MAX = VALUE_ARRAY + knownArrayTypes.length + 1; // 1 type is not typed; just an array. const WORD_POS_RESET = 0; const WORD_POS_TRUE_1 = 1; const WORD_POS_TRUE_2 = 2; const WORD_POS_TRUE_3 = 3; const WORD_POS_FALSE_1 = 5; const WORD_POS_FALSE_2 = 6; const WORD_POS_FALSE_3 = 7; const WORD_POS_FALSE_4 = 8; const WORD_POS_NULL_1 = 9; const WORD_POS_NULL_2 = 10; const WORD_POS_NULL_3 = 11; const WORD_POS_UNDEFINED_1 = 12; const WORD_POS_UNDEFINED_2 = 13; const WORD_POS_UNDEFINED_3 = 14; const WORD_POS_UNDEFINED_4 = 15; const WORD_POS_UNDEFINED_5 = 16; const WORD_POS_UNDEFINED_6 = 17; const WORD_POS_UNDEFINED_7 = 18; const WORD_POS_UNDEFINED_8 = 19; const WORD_POS_NAN_1 = 20; const WORD_POS_NAN_2 = 21; const WORD_POS_INFINITY_1 = 22; const WORD_POS_INFINITY_2 = 23; const WORD_POS_INFINITY_3 = 24; const WORD_POS_INFINITY_4 = 25; const WORD_POS_INFINITY_5 = 26; const WORD_POS_INFINITY_6 = 27; const WORD_POS_INFINITY_7 = 28; const WORD_POS_FIELD = 29; const WORD_POS_AFTER_FIELD = 30; const WORD_POS_END = 31; const WORD_POS_AFTER_FIELD_VALUE = 32; //const WORD_POS_BINARY = 32; const CONTEXT_UNKNOWN = 0 const CONTEXT_IN_ARRAY = 1 const CONTEXT_OBJECT_FIELD = 2 const CONTEXT_OBJECT_FIELD_VALUE = 3 const CONTEXT_CLASS_FIELD = 4 const CONTEXT_CLASS_VALUE = 5 const CONTEXT_CLASS_FIELD_VALUE = 6 const keywords = { ["true"]:true,["false"]:false,["null"]:null,["NaN"]:NaN,["Infinity"]:Infinity,["undefined"]:undefined } /* Extend Date type with a nanosecond field. */ class DateNS extends Date { constructor(a,b ) { super(a); this.ns = b||0; } } JSOX.DateNS = DateNS; const contexts = []; function getContext() { let ctx = contexts.pop(); if( !ctx ) ctx = { context : CONTEXT_UNKNOWN , current_proto : null , current_class : null , current_class_field : 0 , arrayType : -1 , valueType : VALUE_UNSET , elements : null }; return ctx; } function dropContext(ctx) { contexts.push( ctx ) } JSOX.updateContext = function() { //if( toProtoTypes.get( Map.prototype ) ) return; //console.log( "Do init protoypes for new context objects..." ); //initPrototypes(); } const buffers = []; function getBuffer() { let buf = buffers.pop(); if( !buf ) buf = { buf:null, n:0 }; else buf.n = 0; return buf; } function dropBuffer(buf) { buffers.push( buf ); } /** * @param {string} string * @returns {string} */ JSOX.escape = function(string) { let n; let output = ''; if( !string ) return string; for( n = 0; n < string.length; n++ ) { if( ( string[n] == '"' ) || ( string[n] == '\\' ) || ( string[n] == '`' )|| ( string[n] == '\'' )) { output += '\\'; } output += string[n]; } return output; } let toProtoTypes = new WeakMap(); let toObjectTypes = new Map(); let fromProtoTypes = new Map(); let commonClasses = []; JSOX.reset = resetJSOX; function resetJSOX() { toProtoTypes = new WeakMap(); toObjectTypes = new Map(); fromProtoTypes = new Map(); commonClasses = []; } /** * @param {(value:any)} [cb] * @param {(this: unknown, key: string, value: unknown) => any} [reviver] * @returns {none} */ JSOX.begin = function( cb, reviver ) { const val = { name : null, // name of this value (if it's contained in an object) value_type: VALUE_UNSET, // value from above indiciating the type of this value string : '', // the string value of this value (strings and number types only) contains : null, className : null, }; const pos = { line:1, col:1 }; let n = 0; let str; let localFromProtoTypes = new Map(); let word = WORD_POS_RESET, status = true, redefineClass = false, negative = false, result = null, rootObject = null, elements = undefined, context_stack = { first : null, last : null, saved : null, push(node) { //_DEBUG_PARSING_CONTEXT && console.log( "pushing context:", node ); let recover = this.saved; if( recover ) { this.saved = recover.next; recover.node = node; recover.next = null; recover.prior = this.last; } else { recover = { node : node, next : null, prior : this.last }; } if( !this.last ) this.first = recover; else this.last.next = recover; this.last = recover; this.length++; }, pop() { let result = this.last; // through normal usage this line can never be used. //if( !result ) return null; if( !(this.last = result.prior ) ) this.first = null; result.next = this.saved; if( this.last ) this.last.next = null; if( !result.next ) result.first = null; this.saved = result; this.length--; //_DEBUG_PARSING_CONTEXT && console.log( "popping context:", result.node ); return result.node; }, length : 0, /*dump() { // //_DEBUG_CONTEXT_STACK console.log( "STACK LENGTH:", this.length ); let cur= this.first; let level = 0; while( cur ) { console.log( "Context:", level, cur.node ); level++; cur = cur.next; } }*/ }, classes = [], // class templates that have been defined. protoTypes = {}, current_proto = null, // the current class being defined or being referenced. current_class = null, // the current class being defined or being referenced. current_class_field = 0, arrayType = -1, // the current class being defined or being referenced. parse_context = CONTEXT_UNKNOWN, comment = 0, fromHex = false, decimal = false, exponent = false, exponent_sign = false, exponent_digit = false, inQueue = { first : null, last : null, saved : null, push(node) { let recover = this.saved; if( recover ) { this.saved = recover.next; recover.node = node; recover.next = null; recover.prior = this.last; } else { recover = { node : node, next : null, prior : this.last }; } if( !this.last ) this.first = recover; else this.last.next = recover; this.last = recover; }, shift() { let result = this.first; if( !result ) return null; if( !(this.first = result.next ) ) this.last = null; result.next = this.saved; this.saved = result; return result.node; }, unshift(node) { let recover = this.saved; // this is always true in this usage. //if( recover ) { this.saved = recover.next; recover.node = node; recover.next = this.first; recover.prior = null; //} //else { recover = { node : node, next : this.first, prior : null }; } if( !this.first ) this.last = recover; this.first = recover; } }, gatheringStringFirstChar = null, gatheringString = false, gatheringNumber = false, stringEscape = false, cr_escaped = false, unicodeWide = false, stringUnicode = false, stringHex = false, hex_char = 0, hex_char_len = 0, completed = false, date_format = false, isBigInt = false ; function throwEndError( leader ) { throw new Error( `${leader} at ${n} [${pos.line}:${pos.col}]`); } return { /** * Define a class that can be used to deserialize objects of this type. * @param {string} prototypeName * @param {type} o * @param {(any)=>any} f */ fromJSOX( prototypeName, o, f ) { if( localFromProtoTypes.get(prototypeName) ) throw new Error( "Existing fromJSOX has been registered for prototype" ); function privateProto() { } if( !o ) o = privateProto; if( o && !("constructor" in o )){ throw new Error( "Please pass a prototype like thing..."); } localFromProtoTypes.set( prototypeName, { protoCon:o.prototype.constructor, cb:f } ); }, registerFromJSOX( prototypeName, o/*, f*/ ) { throw new Error( "registerFromJSOX is deprecated, please update to use fromJSOX instead:" + prototypeName + o.toString() ); }, finalError() { if( comment !== 0 ) { // most of the time everything's good. if( comment === 1 ) throwEndError( "Comment began at end of document" ); if( comment === 2 ) /*console.log( "Warning: '//' comment without end of line ended document" )*/; if( comment === 3 ) throwEndError( "Open comment '/*' is missing close at end of document" ); if( comment === 4 ) throwEndError( "Incomplete '/* *' close at end of document" ); } if( gatheringString ) throwEndError( "Incomplete string" ); }, value() { this.finalError(); let r = result; result = undefined; return r; }, /** * Reset the parser to a blank state. */ reset() { word = WORD_POS_RESET; status = true; if( inQueue.last ) inQueue.last.next = inQueue.save; inQueue.save = inQueue.first; inQueue.first = inQueue.last = null; if( context_stack.last ) context_stack.last.next = context_stack.save; context_stack.length = 0; context_stack.save = inQueue.first; context_stack.first = context_stack.last = null;//= []; elements = undefined; parse_context = CONTEXT_UNKNOWN; classes = []; protoTypes = {}; current_proto = null; current_class = null; current_class_field = 0; val.value_type = VALUE_UNSET; val.name = null; val.string = ''; val.className = null; pos.line = 1; pos.col = 1; negative = false; comment = 0; completed = false; gatheringString = false; stringEscape = false; // string stringEscape intro cr_escaped = false; // carraige return escaped date_format = false; //stringUnicode = false; // reading \u //unicodeWide = false; // reading \u{} in string //stringHex = false; // reading \x in string }, usePrototype(className,protoType ) { protoTypes[className] = protoType; }, /** * Add input to the parser to get parsed. * @param {string} msg */ write(msg) { let retcode; if (typeof msg !== "string" && typeof msg !== "undefined") msg = String(msg); if( !status ) throw new Error( "Parser is still in an error state, please reset before resuming" ); for( retcode = this._write(msg,false); retcode > 0; retcode = this._write() ) { if( typeof reviver === 'function' ) (function walk(holder, key) { let k, v, value = holder[key]; if (value && typeof value === 'object') { for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = walk(value, k); if (v !== undefined) { value[k] = v; } else { delete value[k]; } } } } return reviver.call(holder, key, value); }({'': result}, '')); result = cb( result ); if( retcode < 2 ) break; } }, /** * Parse a string and return the result. * @param {string} msg * @param {(key:string,value:any)=>any} [reviver] * @returns {any} */ parse(msg,reviver) { if (typeof msg !== "string") msg = String(msg); this.reset(); const writeResult = this._write( msg, true ); if( writeResult > 0 ) { if( writeResult > 1 ){ // probably a carriage return. //console.log( "Extra data at end of message"); } let result = this.value(); if( ( "undefined" === typeof result ) && writeResult > 1 ){ throw new Error( "Pending value could not complete"); } result = typeof reviver === 'function' ? (function walk(holder, key) { let k, v, value = holder[key]; if (value && typeof value === 'object') { for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = walk(value, k); if (v !== undefined) { value[k] = v; } else { delete value[k]; } } } } return reviver.call(holder, key, value); }({'': result}, '')) : result; return result; } this.finalError(); return undefined; return this.write(msg ); }, _write(msg,complete_at_end) { let cInt; let input; let buf; let retval = 0; function throwError( leader, c ) { throw new Error( `${leader} '${String.fromCodePoint( c )}' unexpected at ${n} (near '${buf.substr(n>4?(n-4):0,n>4?3:(n-1))}[${String.fromCodePoint( c )}]${buf.substr(n, 10)}') [${pos.line}:${pos.col}]`); } function RESET_VAL() { val.value_type = VALUE_UNSET; val.string = ''; val.contains = null; //val.className = null; } function convertValue() { let fp = null; //_DEBUG_PARSING && console.log( "CONVERT VAL:", val ); switch( val.value_type ){ case VALUE_NUMBER: //1502678337047 if( ( ( val.string.length > 13 ) || ( val.string.length == 13 && val[0]>'2' ) ) && !date_format && !exponent_digit && !exponent_sign && !decimal ) { isBigInt = true; } if( isBigInt ) { if( hasBigInt ) return BigInt(val.string); else throw new Error( "no builtin BigInt()", 0 ) } if( date_format ) { const r = val.string.match(/\.(\d\d\d\d*)/ ); const frac = ( r )?( r )[1]:null; if( !frac || (frac.length < 4) ) { const r = new Date( val.string ); if(isNaN(r.getTime())) throwError( "Bad Date format", cInt ); return r; } else { let ns = frac.substr( 3 ); while( ns.length < 6 ) ns = ns+'0'; const r = new DateNS( val.string, Number(ns ) ); if(isNaN(r.getTime())) throwError( "Bad DateNS format" + r+r.getTime(), cInt ); return r; } //const r = new Date( val.string ); if(isNaN(r.getTime())) throwError( "Bad number format", cInt ); return r; } return (negative?-1:1) * Number( val.string ); case VALUE_STRING: if( val.className ) { fp = localFromProtoTypes.get( val.className ); if( !fp ) fp = fromProtoTypes.get( val.className ); if( fp && fp.cb ) { val.className = null; return fp.cb.call( val.string ); } else { // '[object Object]' throws this error. throw new Error( "Double string error, no constructor for: new " + val.className + "("+val.string+")" ) } } return val.string; case VALUE_TRUE: return true; case VALUE_FALSE: return false; case VALUE_NEG_NAN: return -NaN; case VALUE_NAN: return NaN; case VALUE_NEG_INFINITY: return -Infinity; case VALUE_INFINITY: return Infinity; case VALUE_NULL: return null; case VALUE_UNDEFINED: return undefined; case VALUE_EMPTY: return undefined; case VALUE_OBJECT: if( val.className ) { //_DEBUG_PARSING_DETAILS && console.log( "class reviver" ); fp = localFromProtoTypes.get( val.className ); if( !fp ) fp = fromProtoTypes.get( val.className ); val.className = null; if( fp && fp.cb ) return val.contains = fp.cb.call( val.contains ); } return val.contains; case VALUE_ARRAY: //_DEBUG_PARSING_DETAILS && console.log( "Array conversion:", arrayType, val.contains ); if( arrayType >= 0 ) { let ab; if( val.contains.length ) ab = DecodeBase64( val.contains[0] ) else ab = DecodeBase64( val.string ); if( arrayType === 0 ) { arrayType = -1; return ab; } else { const newab = new knownArrayTypes[arrayType]( ab ); arrayType = -1; return newab; } } else if( arrayType === -2 ) { let obj = rootObject; //let ctx = context_stack.first; let lvl; //console.log( "Resolving Reference...", context_stack.length ); //console.log( "--elements and array", elements ); const pathlen = val.contains.length; for( lvl = 0; lvl < pathlen; lvl++ ) { const idx = val.contains[lvl]; //_DEBUG_REFERENCES && console.log( "Looking up idx:", idx, "of", val.contains, "in", obj ); let nextObj = obj[idx]; //_DEBUG_REFERENCES && console.log( "Resolve path:", lvl, idx,"in", obj, context_stack.length, val.contains.toString() ); //_DEBUG_REFERENCES && console.log( "NEXT OBJECT:", nextObj ); if( !nextObj ) { { let ctx = context_stack.first; let p = 0; //_DEBUG_PARSING_CONTEXT && context_stack.dump(); while( ctx && p < pathlen && p < context_stack.length ) { const thisKey = val.contains[p]; if( !ctx.next || thisKey !== ctx.next.node.name ) { break; // can't follow context stack any further.... } //_DEBUG_REFERENCES && console.log( "Checking context:", obj, "p=",p, "key=",thisKey, "ctx(and .next)=",util.inspect(ctx)); //console.dir(ctx, { depth: null }) if( ctx.next ) { if( "number" === typeof thisKey ) { const actualObject = ctx.next.node.elements; //_DEBUG_REFERENCES && console.log( "Number in index... tracing stack...", obj, actualObject, ctx && ctx.next && ctx.next.next && ctx.next.next.node ); if( actualObject && thisKey >= actualObject.length ) { //_DEBUG_REFERENCES && console.log( "AT ", p, actualObject.length, val.contains.length ); if( p === (context_stack.length-1) ) { //_DEBUG_REFERENCES && console.log( "This is actually at the current object so use that", p, val.contains, elements ); nextObj = elements; p++; ctx = ctx.next; break; } else { //_DEBUG_REFERENCES && console.log( "is next... ", thisKey, actualObject.length ) if( ctx.next.next && thisKey === actualObject.length ) { //_DEBUG_REFERENCES && console.log( "is next... ") nextObj = ctx.next.next.node.elements; ctx = ctx.next; p++; obj = nextObj; continue; } //_DEBUG_REFERENCES && console.log( "FAILING HERE", ctx.next, ctx.next.next, elements, obj ); //_DEBUG_REFERENCES && console.log( "Nothing after, so this is just THIS?" ); nextObj = elements; p++; // make sure to exit. break; //obj = next } } } else { //_DEBUG_REFERENCES && console.log( "field AT index", p,"of", val.contains.length ); if( thisKey !== ctx.next.node.name ){ //_DEBUG_REFERENCES && console.log( "Expect:", thisKey, ctx.next.node.name, ctx.next.node.elements ); nextObj = ( ctx.next.node.elements[thisKey] ); //throw new Error( "Unexpected path-context relationship" ); lvl = p; break; } else { //_DEBUG_REFERENCES && console.log( "Updating next object(NEW) to", ctx.next.node, elements, thisKey) if( ctx.next.next ) nextObj = ctx.next.next.node.elements; else { //_DEBUG_REFERENCES && console.log( "Nothing after, so this is just THIS?" ); nextObj = elements; } //_DEBUG_REFERENCES && console.log( "using named element from", ctx.next.node.elements, "=", nextObj ) } } //if( //_DEBUG_REFERENCES ) { // const a = ctx.next.node.elements; // console.log( "Stack Dump:" // , a?a.length:a // , ctx.next.node.name // , thisKey // ); //} } else { nextObj = nextObj[thisKey]; } //_DEBUG_REFERENCES && console.log( "Doing next context??", p, context_stack.length, val.contains.length ); ctx = ctx.next; p++; } //_DEBUG_REFERENCES && console.log( "Done with context stack...level", lvl, "p", p ); if( p < pathlen ) lvl = p-1; else lvl = p; } //_DEBUG_REFERENCES && console.log( "End of processing level:", lvl ); } if( ("object" === typeof nextObj ) && !nextObj ) { throw new Error( "Path did not resolve properly:" + val.contains + " at " + idx + '(' + lvl + ')' ); } obj = nextObj; } //_DEBUG_PARSING && console.log( "Resulting resolved object:", obj ); //_DEBUG_PARSING_DETAILS && console.log( "SETTING MODE TO -3 (resolved -2)" ); arrayType = -3; return obj; } if( val.className ) { fp = localFromProtoTypes.get( val.className ); if( !fp ) fp = fromProtoTypes.get( val.className ); val.className = null; if( fp && fp.cb ) return fp.cb.call( val.contains ); } return val.contains; default: console.log( "Unhandled value conversion.", val ); break; } } function arrayPush() { //_DEBUG_PARSING && console.log( "PUSH TO ARRAY:", val ); if( arrayType == -3 ) { //_DEBUG_PARSING && console.log(" Array type -3?", val.value_type, elements ); if( val.value_type === VALUE_OBJECT ) { elements.push( val.contains ); } arrayType = -1; // next one should be allowed? return; } //else // console.log( "Finally a push that's not already pushed!", ); switch( val.value_type ){ case VALUE_EMPTY: elements.push( undefined ); delete elements[elements.length-1]; break; default: elements.push( convertValue() ); break; } RESET_VAL(); } function objectPush() { if( arrayType === -3 && val.value_type === VALUE_ARRAY ) { //console.log( "Array has already been set in object." ); //elements[val.name] = val.contains; RESET_VAL(); arrayType = -1; return; } if( val.value_type === VALUE_EMPTY ) return; if( !val.name && current_class ) { //_DEBUG_PARSING_DETAILS && console.log( "A Stepping current class field:", current_class_field, val.name ); val.name = current_class.fields[current_class_field++]; } let value = convertValue(); if( current_proto && current_proto.protoDef && current_proto.protoDef.cb ) { //_DEBUG_PARSING_DETAILS && console.log( "SOMETHING SHOULD AHVE BEEN REPLACED HERE??", current_proto ); //_DEBUG_PARSING_DETAILS && console.log( "(need to do fromprototoypes here) object:", val, value ); value = current_proto.protoDef.cb.call( elements, val.name, value ); if( value ) elements[val.name] = value; //elements = new current_proto.protoCon( elements ); }else { //_DEBUG_PARSING_DETAILS && console.log( "Default no special class reviver", val.name, value ); elements[val.name] = value; } //_DEBUG_PARSING_DETAILS && console.log( "Updated value:", current_class_field, val.name, elements[val.name] ); //_DEBUG_PARSING && console.log( "+++ Added object field:", val.name, elements, elements[val.name], rootObject ); RESET_VAL(); } function recoverIdent(cInt) { //_DEBUG_PARSING&&console.log( "Recover Ident char:", cInt, val, String.fromCodePoint(cInt), "word:", word ); if( word !== WORD_POS_RESET ) { if( negative ) { //val.string += "-"; negative = false; throwError( "Negative outside of quotes, being converted to a string (would lose count of leading '-' characters)", cInt ); } switch( word ) { case WORD_POS_END: switch( val.value_type ) { case VALUE_TRUE: val.string += "true"; break case VALUE_FALSE: val.string += "false"; break case VALUE_NULL: val.string += "null"; break case VALUE_INFINITY: val.string += "Infinity"; break case VALUE_NEG_INFINITY: val.string += "-Infinity"; throwError( "Negative outside of quotes, being converted to a string", cInt ); break case VALUE_NAN: val.string += "NaN"; break case VALUE_NEG_NAN: val.string += "-NaN"; throwError( "Negative outside of quotes, being converted to a string", cInt ); break case VALUE_UNDEFINED: val.string += "undefined"; break case VALUE_STRING: break; case VALUE_UNSET: break; default: console.log( "Value of type " + val.value_type + " is not restored..." ); } break; case WORD_POS_TRUE_1 : val.string += "t"; break; case WORD_POS_TRUE_2 : val.string += "tr"; break; case WORD_POS_TRUE_3 : val.string += "tru"; break; case WORD_POS_FALSE_1 : val.string += "f"; break; case WORD_POS_FALSE_2 : val.string += "fa"; break; case WORD_POS_FALSE_3 : val.string += "fal"; break; case WORD_POS_FALSE_4 : val.string += "fals"; break; case WORD_POS_NULL_1 : val.string += "n"; break; case WORD_POS_NULL_2 : val.string += "nu"; break; case WORD_POS_NULL_3 : val.string += "nul"; break; case WORD_POS_UNDEFINED_1 : val.string += "u"; break; case WORD_POS_UNDEFINED_2 : val.string += "un"; break; case WORD_POS_UNDEFINED_3 : val.string += "und"; break; case WORD_POS_UNDEFINED_4 : val.string += "unde"; break; case WORD_POS_UNDEFINED_5 : val.string += "undef"; break; case WORD_POS_UNDEFINED_6 : val.string += "undefi"; break; case WORD_POS_UNDEFINED_7 : val.string += "undefin"; break; case WORD_POS_UNDEFINED_8 : val.string += "undefine"; break; case WORD_POS_NAN_1 : val.string += "N"; break; case WORD_POS_NAN_2 : val.string += "Na"; break; case WORD_POS_INFINITY_1 : val.string += "I"; break; case WORD_POS_INFINITY_2 : val.string += "In"; break; case WORD_POS_INFINITY_3 : val.string += "Inf"; break; case WORD_POS_INFINITY_4 : val.string += "Infi"; break; case WORD_POS_INFINITY_5 : val.string += "Infin"; break; case WORD_POS_INFINITY_6 : val.string += "Infini"; break; case WORD_POS_INFINITY_7 : val.string += "Infinit"; break; case WORD_POS_RESET : break; case WORD_POS_FIELD : break; case WORD_POS_AFTER_FIELD: //throwError( "String-keyword recovery fail (after whitespace)", cInt); break; case WORD_POS_AFTER_FIELD_VALUE: throwError( "String-keyword recovery fail (after whitespace)", cInt ); break; default: //console.log( "Word context: " + word + " unhandled" ); } val.value_type = VALUE_STRING; if( word < WORD_POS_FIELD) word = WORD_POS_END; } else { word = WORD_POS_END; //if( val.value_type === VALUE_UNSET && val.string.length ) val.value_type = VALUE_STRING } if( cInt == 123/*'{'*/ ) openObject(); else if( cInt == 91/*'['*/ ) openArray(); else if( cInt == 44/*','*/ ) { // comma separates the string, it gets consumed. } else { // ignore white space. if( cInt == 32/*' '*/ || cInt == 13 || cInt == 10 || cInt == 9 || cInt == 0xFEFF || cInt == 0x2028 || cInt == 0x2029 ) { //_DEBUG_WHITESPACE && console.log( "IGNORE WHITESPACE" ); return; } if( cInt == 44/*','*/ || cInt == 125/*'}'*/ || cInt == 93/*']'*/ || cInt == 58/*':'*/ ) throwError( "Invalid character near identifier", cInt ); else //if( typeof cInt === "number") val.string += str; } //console.log( "VAL STRING IS:", val.string, str ); } // gather a string from an input stream; start_c is the opening quote to find a related close quote. function gatherString( start_c ) { let retval = 0; while( retval == 0 && ( n < buf.length ) ) { str = buf.charAt(n); let cInt = buf.codePointAt(n++); if( cInt >= 0x10000 ) { str += buf.charAt(n); n++; } //console.log( "gathering....", stringEscape, str, cInt, unicodeWide, stringHex, stringUnicode, hex_char_len ); pos.col++; if( cInt == start_c ) { //( cInt == 34/*'"'*/ ) || ( cInt == 39/*'\''*/ ) || ( cInt == 96/*'`'*/ ) ) if( stringEscape ) { if( stringHex ) throwError( "Incomplete hexidecimal sequence", cInt ); else if( stringUnicode ) throwError( "Incomplete long unicode sequence", cInt ); else if( unicodeWide ) throwError( "Incomplete unicode sequence", cInt ); if( cr_escaped ) { cr_escaped = false; retval = 1; // complete string, escaped \r } else val.string += str; stringEscape = false; } else { // quote matches, and is not processing an escape sequence. retval = 1; } } else if( stringEscape ) { if( unicodeWide ) { if( cInt == 125/*'}'*/ ) { val.string += String.fromCodePoint( hex_char ); unicodeWide = false; stringUnicode = false; stringEscape = false; continue; } hex_char *= 16; if( cInt >= 48/*'0'*/ && cInt <= 57/*'9'*/ ) hex_char += cInt - 0x30; else if( cInt >= 65/*'A'*/ && cInt <= 70/*'F'*/ ) hex_char += ( cInt - 65 ) + 10; else if( cInt >= 97/*'a'*/ && cInt <= 102/*'f'*/ ) hex_char += ( cInt - 97 ) + 10; else { throwError( "(escaped character, parsing hex of \\u)", cInt ); retval = -1; unicodeWide = false; stringEscape = false; continue; } continue; } else if( stringHex || stringUnicode ) { if( hex_char_len === 0 && cInt === 123/*'{'*/ ) { unicodeWide = true; continue; } if( hex_char_len < 2 || ( stringUnicode && hex_char_len < 4 ) ) { hex_char *= 16; if( cInt >= 48/*'0'*/ && cInt <= 57/*'9'*/ ) hex_char += cInt - 0x30; else if( cInt >= 65/*'A'*/ && cInt <= 70/*'F'*/ ) hex_char += ( cInt - 65 ) + 10; else if( cInt >= 97/*'a'*/ && cInt <= 102/*'f'*/ ) hex_char += ( cInt - 97 ) + 10; else { throwError( stringUnicode?"(escaped character, parsing hex of \\u)":"(escaped character, parsing hex of \\x)", cInt ); retval = -1; stringHex = false; stringEscape = false; continue; } hex_char_len++; if( stringUnicode ) { if( hex_char_len == 4 ) { val.string += String.fromCodePoint( hex_char ); stringUnicode = false; stringEscape = false; } } else if( hex_char_len == 2 ) { val.string += String.fromCodePoint( hex_char ); stringHex = false; stringEscape = false; } continue; } } switch( cInt ) { case 13/*'\r'*/: cr_escaped = true; pos.col = 1; continue; case 0x2028: // LS (Line separator) case 0x2029: // PS (paragraph separate) pos.col = 1; // falls through case 10/*'\n'*/: if( !cr_escaped ) { // \\ \n pos.col = 1; } else { // \\ \r \n cr_escaped = false; } pos.line++; break; case 116/*'t'*/: val.string += '\t'; break; case 98/*'b'*/: val.string += '\b'; break; case 110/*'n'*/: val.string += '\n'; break; case 114/*'r'*/: val.string += '\r'; break; case 102/*'f'*/: val.string += '\f'; break; case 118/*'v'*/: val.string += '\v'; break; case 48/*'0'*/: val.string += '\0'; break; case 120/*'x'*/: stringHex = true; hex_char_len = 0; hex_char = 0; continue; case 117/*'u'*/: stringUnicode = true; hex_char_len = 0; hex_char = 0; continue; //case 47/*'/'*/: //case 92/*'\\'*/: //case 34/*'"'*/: //case 39/*"'"*/: //case 96/*'`'*/: default: val.string += str; break; } //console.log( "other..." ); stringEscape = false; } else if( cInt === 92/*'\\'*/ ) { if( stringEscape ) { val.string += '\\'; stringEscape = false } else { stringEscape = true; hex_char = 0; hex_char_len = 0; } } else { /* any other character */ if( cr_escaped ) { // \\ \r <any char> cr_escaped = false; pos.line++; pos.col = 2; // this character is pos 1; and increment to be after it. } val.string += str; } } return retval; } // gather a number from the input stream. function collectNumber() { let _n; while( (_n = n) < buf.length ) { str = buf.charAt(_n); let cInt = buf.codePointAt(n++); if( cInt >= 256 ) { pos.col -= n - _n; n = _n; // put character back in queue to process. break; } else { //_DEBUG_PARSING_NUMBERS && console.log( "in getting number:", n, cInt, String.fromCodePoint(cInt) ); if( cInt == 95 /*_*/ ) continue; pos.col++; // leading zeros should be forbidden. if( cInt >= 48/*'0'*/ && cInt <= 57/*'9'*/ ) { if( exponent ) { exponent_digit = true; } val.string += str; } else if( cInt == 45/*'-'*/ || cInt == 43/*'+'*/ ) { if( val.string.length == 0 || ( exponent && !exponent_sign && !exponent_digit ) ) { if( cInt == 45/*'-'*/ && !exponent ) negative = !negative; val.string += str; exponent_sign = true; } else { if( negative ) { val.string = '-' + val.string; negative = false; } val.string += str; date_format = true; } } else if( cInt == 78/*'N'*/ ) { if( word == WORD_POS_RESET ) { gatheringNumber = false; word = WORD_POS_NAN_1; return; } throwError( "fault while parsing number;", cInt ); break; } else if( cInt == 73/*'I'*/ ) { if( word == WORD_POS_RESET ) { gatheringNumber = false; word = WORD_POS_INFINITY_1; return; } throwError( "fault while parsing number;", cInt ); break; } else if( cInt == 58/*':'*/ && date_format ) { if( negative ) { val.string = '-' + val.string; negative = false; } val.string += str; date_format = true; } else if( cInt == 84/*'T'*/ && date_format ) { if( negative ) { val.string = '-' + val.string; negative = false; } val.string += str; date_format = true; } else if( cInt == 90/*'Z'*/ && date_format ) { if( negative ) { val.string = '-' + val.string; negative = false; } val.string += str; date_format = true; } else if( cInt == 46/*'.'*/ ) { if( !decimal && !fromHex && !exponent ) { val.string += str; decimal = true; } else { status = false; throwError( "fault while parsing number;", cInt ); break; } } else if( cInt == 110/*'n'*/ ) { isBigInt = true; break; } else if( fromHex && ( ( ( cInt >= 95/*'a'*/ ) && ( cInt <= 102/*'f'*/ ) ) || ( ( cInt >= 65/*'A'*/ ) && ( cInt <= 70/*'F'*/ ) ) ) ) { val.string += str; } else if( cInt == 120/*'x'*/ || cInt == 98/*'b'*/ || cInt == 111/*'o'*/ || cInt == 88/*'X'*/ || cInt == 66/*'B'*/ || cInt == 79/*'O'*/ ) { // hex conversion. if( !fromHex && val.string == '0' ) { fromHex = true; val.string += str; } else { status = false; throwError( "fault while parsing number;", cInt ); break; } } else if( ( cInt == 101/*'e'*/ ) || ( cInt == 69/*'E'*/ ) ) { if( !exponent ) { val.string += str; exponent = true; } else { status = false; throwError( "fault while parsing number;", cInt ); break; } } else { if( cInt == 32/*' '*/ || cInt == 13 || cInt == 10 || cInt == 9 || cInt == 47/*'/'*/ || cInt == 35/*'#'*/ || cInt == 44/*','*/ || cInt == 125/*'}'*/ || cInt == 93/*']'*/ || cInt == 123/*'{'*/ || cInt == 91/*'['*/ || cInt == 34/*'"'*/ || cInt == 39/*'''*/ || cInt == 96/*'`'*/ || cInt == 58/*':'*/ ) { pos.col -= n - _n; n = _n; // put character back in queue to process. break; } else { if( complete_at_end ) { status = false; throwError( "fault while parsing number;", cInt ); } break; } } } } if( (!complete_at_end) && n == buf.length ) { gatheringNumber = true; } else { gatheringNumber = false; val.value_type = VALUE_NUMBER; if( parse_context == CONTEXT_UNKNOWN ) { completed = true; } } } // begin parsing an object type function openObject() { let nextMode = CONTEXT_OBJECT_FIELD; let cls = null; let tmpobj = {}; //_DEBUG_PARSING && console.log( "opening object:", val.string, val.value_type, word, parse_context ); if( word > WORD_POS_RESET && word < WORD_POS_FIELD ) recoverIdent( 123 /* '{' */ ); let protoDef; protoDef = getProto(); // lookup classname using val.string and get protodef(if any) if( parse_context == CONTEXT_UNKNOWN ) { if( word == WORD_POS_FIELD /*|| word == WORD_POS_AFTER_FIELD*/ || word == WORD_POS_END && ( protoDef || val.string.length ) ) { if( protoDef && protoDef.protoDef && protoDef.protoDef.protoCon ) { tmpobj = new protoDef.protoDef.protoCon(); } if( !protoDef || !protoDef.protoDef && val.string ) // class creation is redundant... { cls = classes.find( cls=>cls.name===val.string ); //console.log( "Probably creating the Macro-Tag here?", cls ) if( !cls ) { /* eslint-disable no-inner-declarations */ function privateProto() {} // this just uses the tmpobj {} container to store the values collected for this class... // this does not generate the instance of the class. // if this tag type is also a prototype, use that prototype, else create a unique proto // for this tagged class type. classes.push( cls = { name : val.string , protoCon: (protoDef && protoDef.protoDef && protoDef.protoDef.protoCon) || privateProto.constructor , fields : [] } ); nextMode = CONTEXT_CLASS_FIELD; } else if( redefineClass ) { //_DEBUG_PARSING && console.log( "redefine class..." ); // redefine this class cls.fields.length = 0; nextMode = CONTEXT_CLASS_FIELD; } else { //_DEBUG_PARSING && console.log( "found existing class, using it...."); tmpobj = new cls.protoCon(); //tmpobj = Object.assign( tmpobj, cls.protoObject ); //Object.setPrototypeOf( tmpobj, Object.getPrototypeOf( cls.protoObject ) ); nextMode = CONTEXT_CLASS_VALUE; } redefineClass = false; } current_class = cls word = WORD_POS_RESET; } else { word = WORD_POS_FIELD; } } else if( word == WORD_POS_FIELD /*|| word == WORD_POS_AFTER_FIELD*/ || parse_context === CONTEXT_IN_ARRAY || parse_context === CONTEXT_OBJECT_FIELD_VALUE || parse_context == CONTEXT_CLASS_VALUE ) { if( word != WORD_POS_RESET || val.value_type == VALUE_STRING ) { if( protoDef && protoDef.protoDef ) { // need to collect the object, tmpobj = new protoDef.protoDef.protoCon(); } else { // look for a class type (shorthand) to recover. cls = classes.find( cls=>cls.name === val.string ); if( !cls ) { /* eslint-disable no-inner-declarations */ function privateProto(){} //sconsole.log( "privateProto has no proto?", privateProto.prototype.constructor.name ); localFromProtoTypes.set( val.string, { protoCon:privateProto.prototype.constructor , cb: null } ); tmpobj = new privateProto(); } else { nextMode = CONTEXT_CLASS_VALUE; tmpobj = {}; } } //nextMode = CONTEXT_CLASS_VALUE; word = WORD_POS_RESET; } else { word = WORD_POS_RESET; } } else if( ( parse_context == CONTEXT_OBJECT_FIELD && word == WORD_POS_RESET ) ) { throwError( "fault while parsing; getting field name unexpected ", cInt ); status = false; return false; } // common code to push into next context let old_context = getContext(); //_DEBUG_PARSING && console.log( "Begin a new object; previously pushed into elements; but wait until trailing comma or close previously ", val.value_type, val.className ); val.value_type = VALUE_OBJECT; if( parse_context === CONTEXT_UNKNOWN ){ elements = tmpobj; } else if( parse_context == CONTEXT_IN_ARRAY ) { if( arrayType == -1 ) { // this is pushed later... //console.log( "PUSHING OPEN OBJECT INTO EXISTING ARRAY - THIS SHOULD BE RE-SET?", JSOX.stringify(context_stack.first.node) ); //elements.push( tmpobj ); } val.name = elements.length; //else if( //_DEBUG_PARSING && arrayType !== -3 ) // console.log( "This is an invalid parsing state, typed array with sub-object elements" ); } else if( parse_context == CONTEXT_OBJECT_FIELD_VALUE || parse_context == CONTEXT_CLASS_VALUE ) { if( !val.name && current_class ){ val.name = current_class.fields[current_class_field++]; //_DEBUG_PARSING_DETAILS && console.log( "B Stepping current class field:", val, current_class_field, val.name ); } //_DEBUG_PARSING_DETAILS && console.log( "Setting element:", val.name, tmpobj ); elements[val.name] = tmpobj; } old_context.context = parse_context; old_context.elements = elements; //old_context.element_array = element_array; old_context.name = val.name; //_DEBUG_PARSING_DETAILS && console.log( "pushing val.name:", val.name, arrayType ); old_context.current_proto = current_proto; old_context.current_class = current_class; old_context.current_class_field = current_class_field; old_context.valueType = val.value_type; old_context.arrayType = arrayType; // pop that we don't want to have this value re-pushed. old_context.className = val.className; //arrayType = -3; // this doesn't matter, it's an object state, and a new array will reset to -1 val.className = null; val.name = null; current_proto = protoDef; current_class = cls; //console.log( "Setting current class:", current_class.name ); current_class_field = 0; elements = tmpobj; if( !rootObject ) rootObject = elements; //_DEBUG_PARSING_STACK && console.log( "push context (open object): ", context_stack.length, " new mode:", nextMode ); context_stack.push( old_context ); //_DEBUG_PARSING_DETAILS && console.log( "RESET OBJECT FIELD", old_context, context_stack ); RESET_VAL(); parse_context = nextMode; return true; } function openArray() { //_DEBUG_PARSING_DETAILS && console.log( "openArray()..." ); if( word > WORD_POS_RESET && word < WORD_POS_FIELD ) recoverIdent( 91 ); if( word == WORD_POS_END && val.string.length ) { //_DEBUG_PARSING && console.log( "recover arrayType:", arrayType, val.string ); let typeIndex = knownArrayTypeNames.findIndex( type=>(type === val.string) ); word = WORD_POS_RESET; if( typeIndex >= 0 ) { arrayType = typeIndex; val.className = val.string; val.string = null; } else { if( val.string === "ref" ) { val.className = null; //_DEBUG_PARSING_DETAILS && console.log( "This will be a reference recovery for key:", val ); arrayType = -2; } else { if( localFromProtoTypes.get( val.string ) ) { val.className = val.string; } else if( fromProtoTypes.get( val.string ) ) { val.className = val.string; } else throwError( `Unknown type '${val.string}' specified for array`, cInt ); //_DEBUG_PARSING_DETAILS && console.log( " !!!!!A Set Classname:", val.className ); } } } else if( parse_context == CONTEXT_OBJECT_FIELD || word == WORD_POS_FIELD || word == WORD_POS_AFTER_FIELD ) { throwError( "Fault while parsing; while getting field name unexpected", cInt ); status = false; return false; } { let old_context = getContext(); //_DEBUG_PARSING && console.log( "Begin a new array; previously pushed into elements; but wait until trailing comma or close previously ", val.value_type ); //_DEBUG_PARSING_DETAILS && console.log( "Opening array:", val, parse_context ); val.value_type = VALUE_ARRAY; let tmparr = []; if( parse_context == CONTEXT_UNKNOWN ) elements = tmparr; else if( parse_context == CONTEXT_IN_ARRAY ) { if( arrayType == -1 ){ //console.log( "Pushing new opening array into existing array already RE-SET" ); elements.push( tmparr ); } //else if( //_DEBUG_PARSING && arrayType !== -3 ) val.name = elements.length; // console.log( "This is an invalid parsing state, typed array with sub-array elements" ); } else if( parse_context == CONTEXT_OBJECT_FIELD_VALUE ) { if( !val.name ) { console.log( "This says it's resolved......." ); arrayType = -3; } if( current_proto && current_proto.protoDef ) { //_DEBUG_PARSING_DETAILS && console.log( "SOMETHING SHOULD HAVE BEEN REPLACED HERE??", current_proto ); //_DEBUG_PARSING_DETAILS && console.log( "(need to do fromprototoypes here) object:", val, value ); if( current_proto.protoDef.cb ){ const newarr = current_proto.protoDef.cb.call( elements, val.name, tmparr ); if( newarr !== undefined ) tmparr = elements[val.name] = newarr; //else console.log( "Warning: Received undefined for an array; keeping original array, not setting field" ); }else elements[val.name] = tmparr; } else elements[val.name] = tmparr; } old_context.context = parse_context; old_context.elements = elements; //old_context.element_array = element_array; old_context.name = val.name; old_context.current_proto = current_proto; old_context.current_class = current_class; old_context.current_class_field = current_class_field; // already pushed? old_context.valueType = val.value_type; old_context.arrayType = (arrayType==-1)?-3:arrayType; // pop that we don't want to have this value re-pushed. old_context.className = val.className; arrayType = -1; val.className = null; //_DEBUG_PARSING_DETAILS && console.log( " !!!!!B Clear Classname:", old_context, val.className, old_context.className, old_context.name ); val.name = null; current_proto = null; current_class = null; current_class_field = 0; //element_array = tmparr; elements = tmparr; if( !rootObject ) rootObject = tmparr; //_DEBUG_PARSING_STACK && console.log( "push context (open array): ", context_stack.length ); context_stack.push( old_context ); //_DEBUG_PARSING_DETAILS && console.log( "RESET ARRAY FIELD", old_context, context_stack ); RESET_VAL(); parse_context = CONTEXT_IN_ARRAY; } return true; } function getProto() { const result = {protoDef:null,cls:null}; if( ( result.protoDef = localFromProtoTypes.get( val.string ) ) ) { if( !val.className ){ val.className = val.string; val.string = null; } // need to collect the object, } else if( ( result.protoDef = fromProtoTypes.get( val.string ) ) ) { if( !val.className ){ val.className = val.string; val.string = null; } } if( val.string ) { result.cls = classes.find( cls=>cls.name === val.string ); if( !result.protoDef && !result.cls ) { // this will creaet a class def with a new proto to cover when we don't KNOW. //throwError( "Referenced class " + val.string + " has not been defined", cInt ); } } return (result.protoDef||result.cls)?result:null; } i