jsox
Version:
Java Script Object eXchange.
1,390 lines (1,336 loc) • 110 kB
JavaScript
//"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