47,48c47,48
<     version:    "1.6",
<     time_stamp: "2018-11-19 12:28:43",
---
>     version:    "1.7",
>     time_stamp: "2019-03-07 07:24:12",
53c53,54
<   this.debugging = false
---
>   this.debugging = true
>   this.propagate = false
113a115,118
>   // Error propogation turned on
>   if (this.propagate)
>     throw e
> 
116,124c121
<   try
<   {
<     if (caller != null)
<     {
<       var result = caller.toString().match(/^function\s+(\S+)\s*\([^\)]*\)/)
<       if (result != null) out = "Error caught by "+result[1]
<     }
<   } catch(e) {}
< 
---
>   var showCaller = true
125a123
>   var showStack = false
130a129,132
>     } else if (args[i] == ":include-stack:") {
>       showStack = true
>     } else if (args[i] == ":suppress-caller:") {
>       showCaller = false
139,141c141,153
<   if (out != "") out += "\n"
<   out += e.name
<   if (showLocation && this.defined(e.lineNumber))
---
>   try
>   {
>     if (caller != null && showCaller)
>     {
>       var result = caller.toString().match(/^function\s+(\S+)\s*\([^\)]*\)/)
>       if (result != null) out = "Error caught by "+result[1]
>     }
>   } catch(e) {}
>   if (typeof(e) != 'object')
>     e = { message: e }
> 
>   if ("name" in e) out += "\n" + e.name
>   if (showLocation && "lineNumber" in e)
144c156
<     if (this.defined(e.fileName)) out += " of file " + e.fileName
---
>     if ("fileName" in e) out += " of file " + e.fileName
146,148c158,163
<   if (this.defined(e.message)) out += "\n" + e.message
<   else if (this.defined(e.description)) out += "\n" + e.description
<   if (this.defined(e.stack))
---
>   if ("message" in e) out += "\n" + e.message
>   else if ("description" in e) out += "\n" + e.description
>   if (out.substr(0,1) == "\n") out = out.substr(1)
>   if (showStack && "stack" in e)
>     out = out + "\n" + e.stack
>   if ("stack" in e)
193c208
<   Exelana_utility.prototype.__extend_DOM_classes__ = function()
---
> Exelana_utility.prototype.__extend_DOM_classes__ = function()
214,221d228
<   /* ********** Error.formatError ***************************************************************
<   String Error.formatError()
<   
<   Creates an error message from an Error object.
<   
<   try { somethingDangerous() } catch(e) { alert(e.formatError()) }
<   */
<   
225,234d231
<     this.addPrototypeFunction(Error,"formatError",
<       function()
<       {
<         var caller = null
<         try { if ("callee" in arguments) caller = arguments.callee.caller } catch(e) {}
<   
<         var args = []
<         for (var i = 0; i < arguments.length; i++) args.push(arguments[i])
<         return Exelana_utility.prototype.__formatError__(this,args,caller)
<       })
237,244d233
<   /* ********** HTMLElement.bottomOf ************************************************************
<   ************* HTMLElement.topOf ***************************************************************
<   Number HTMLElement.bottomOf()
<   Number HTMLElement.topOf()
<   
<   Returns the offset of the bottom or top of the element relative to the document
<   */
<   
248,265d236
<     this.addPrototypeFunction(HTMLElement, "bottomOf",
<       function()
<       {
<         return this.topOf() + this.offsetHeight
<       })
<   
<     this.addPrototypeFunction(HTMLElement, "topOf",
<       function()
<       {
<         var obj = this
<         var n = 0
<         while (obj != null)
<         {
<           n += obj.offsetTop
<           obj = obj.offsetParent
<         }
<         return n
<       })
268,272d238
<   /* ********** Number.comma ********************************************************************
<   String Number.comma()
<   
<   Add thousand separator commas to a number and returns the result
<   */
276,304d241
<     this.addPrototypeFunction(Number, "comma",
<       function()
<       {
<         return (this + '').replace(/(\d)(?=(\d{3})+(?:\.\d*)?$)/g, '$1,')
<       })
<   
<   /* ********** Number.dollar *******************************************************************
<   String Number.dollar()
<   */
<     this.addPrototypeFunction(Number, "dollar",
<       function()
<       {
<         if (this < 0) return "($"+(-1*this).round(2).toFixed(2).comma()+")"
<         return "$"+this.round(2).toFixed(2).comma()
<       })
<   
<   /* ********** Number.round ********************************************************************
<   Number Number.round(digits)
<   
<   Return number rounded to the specified number of digits (like Excel's ROUND function)
<   */
<   
<     this.addPrototypeFunction(Number, "round",
<       function(digits)
<       {
<         if(!Exelana_utility.prototype.defined(digits)) digits = 0
<         var coef = Math.pow(10, digits);
<         return (Math.round(this * coef))/coef
<       })
321,359d257
<   /* ********** String.toHTML *******************************************************************
<   String String.toHTML(space)
<   
<   Returns version of string safe to display in a browser (optionally with non-breaking spaces)
<   */
<     this.addPrototypeFunction(String, "toHTML",
<       function(space='')
<       {
<         return this.replace(/&/g, "&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")
<           .replace(/\\/g, "\\").replace(/ /g, space ? "&nbsp;" : " ")
<       })
<   
<   /* ********** String.trim *********************************************************************
<   ************* String.ltrim ********************************************************************
<   ************* String.rtrim ********************************************************************
<   String String.trim()
<   String String.ltrim()
<   String String.rtrim()
<   
<   String trimming functions from Ten JavaScript Tools by Patrick Hunlock
<      http://www.hunlock.com/blogs/Ten_Javascript_Tools_Everyone_Should_Have
<   */
<     this.addPrototypeFunction(String, "trim",
<       function()
<       {
<         return this.replace(/^\s+|\s+$/g,"")
<       })
<   
<     this.addPrototypeFunction(String, "ltrim",
<       function()
<       {
<         return this.replace(/^\s+/g,"")
<       })
<   
<     this.addPrototypeFunction(String, "rtrim",
<       function()
<       {
<         return this.replace(/\s+$/g,"")
<       })
393a292,302
> /* ********** asHTML  ***************************************************************************
> String asHTML(s,space)
> 
> Returns s as valid HTML (optionally encoding spaces as non-breaking spaces).
> */
> Exelana_utility.prototype.asHTML = function(s,space)
> {
>   space = (space === undefined) ? false : space
>   return s.replace(/&/g, "&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")
>     .replace(/\\/g, "\\").replace(/ /g, space ? "&nbsp;" : " ")
> }
404,405c313
<     var wx = this.whatis(x)
<     if (wx == "string")
---
>     if (typeof(x) == "string")
409c317,319
<       if (wx == "object")
---
>     {
>       var cn = this.className(x)
>       if (cn == "Object")
410a321,322
>       else if (cn == "Date")
>         return x.toString()
412c324,325
<         return "[object "+wx+"]"
---
>         return "[object "+cn+"]"
>     }
479c392,595
<   } catch(e) { this.debugThrow(e.formatError()) }
---
>   } catch(e) {
>     if (this.debugging)
>       this.debugThrow(this.formatError(e))
>     else
>        alert(this.formatError(e))
>   }
> }
> 
> /* ********** className *************************************************************************
> string className(o)
> 
> Return class name of an object
> */
> 
> Exelana_utility.prototype.className = function(o)
> {
>   try
>   {
>     if (typeof(o) != "object" || !("constructor" in o))
>       return null
>     if ("name" in o.constructor)
>       return o.constructor.name
>     var con = o.constructor.toString()
>     var m
>     if (m  = con.match(/^\s*function\s+(\w+)\s*\(/))
>       return m[1]
>     if (m  = con.match(/^\[object\s+(\w+)\]/))
>       return m[1]
>     return null
>   } catch(e) { return null }
> }
> 
> /* ********** clone *****************************************************************************
> clone(thing,deep)
> 
> Creates a copy of something
> 
> The deep argument is a boolean that specifies whether the object should be copied element by
> element. The function runs much faster if deep is false (the default value), but it has a number
> of issues:
>   1. If the thing being copied is an object, the result will have a type of Object and it will
>      not inherit any properties or functions defined in the prototype of the source object.
>   2. It will not correctly copy elements that are Date objects.
>   3. It will not copy elements that are functions.
> 
> If any of these issues are important then pass true for the deep argument to invoke deep
> cloning.
> 
> The deep cloning process will attempt to create an object using the constructor of the source
> object. If that attempt fails, the resulting object will include an additional element
> "__clone_of__" that contains the class name of the source object.
> 
> The deep cloning process supports objects that are self-cloning, that is, the object's prototype
> includes a clone function.
> 
> The deep cloning process takes special care to handle recursion in the object being cloned such
> as in the examples below.
> 
> Example 1:
>   o = {}
>   o.self = o
>   oo = clone(o,true) // oo === oo.self
> 
> Example 2:
>   o = { a: {name: 'test' } }
>   o.b = o.a
>   oo = clone(o,true) // oo.a === oo.b
> */
> 
> Exelana_utility.prototype.clone = function(o,deep)
> {
>   if (deep === undefined || !deep)
>     return JSON.parse(JSON.stringify(o))
> 
>   var state = { oList: [], cList: [], nList:[] }
>   return this.__clone_deep__(o,state,"self",0)
> }
> 
> Exelana_utility.prototype.__clone_deep__ = function(o,state,index,depth)
> {
>   // Handle the simple case
>   if (o == null || typeof(o) != 'object') return o
> 
>   if (depth > 20) debugger
>   var i
>   for (i = 0; i < state.oList.length; i++)
>     if (o === state.oList[i])
>     {
>       console.log(index+" <= "+state.nList[i])
>       return state.cList[i]
>     }
> 
>   var res
>   var name = this.className(o)
>   console.log("clone "+(index != "" ? index+": " : "")+name)
>   if (name == "Array")
>   {
>     res = []
>     for (var i in o)
>       res[i] = this.__clone_deep__(o[i],state,index+"."+i,depth+1)
>     return res
>   } else if (o instanceof Date) {
>     res = new Date()
>     res.setTime(o.getTime())
>     return res
>   } else if (o instanceof HTMLElement) {
>     try
>     {
>       res = document.createElement(o.tagName)
>       console.log("=> HTML "+o.tagName)
>     } catch(e) { res = null }
>     if (res != null)
>     {
>       for (i in o)
>       {
>         // ignore all the elements that are managed by the DOM
>         if (i == "outerHTML" || i == "outerText")
>           continue
>         if (o[i] instanceof CSSStyleDeclaration
>          || o[i] instanceof DOMTokenList
>          || o[i] instanceof HTMLCollection
>          || o[i] instanceof NamedNodeMap
>          || o[i] instanceof Node
>          || o[i] instanceof NodeList)
>            continue
> 
>         try
>         {
>           res[i] = this.__clone_deep__(o[i],state,index+"."+i,depth+1)
>         } catch(e) {
>           console.log(e.message)
>         }
>       }
>       res.innerHTML = o.innerHTML // clone children indirectly
> 
>       // The preceding may have created spurious attributes
>       for (var i = res.attributes.length - 1; i >= 0; i--)
>       {
>         res.attributes.removeNamedItem(res.attributes[i].nodeName)
>       }
>       for (var i = 0; i < o.attributes.length; i++)
>       {
>         res.setAttribute(o.attributes[i].nodeName,o.attributes[i].nodeValue)
> /* maybe version if preceding fails in Internet Explorer
>         var attr = document.createAttribute(o.attributes[i].nodeName)
>         attr.value = o.attributes[i].nodeValue
>         res.attributes.setNamedItem(attr)
> */
>       }
> /* old version that fails in Internet Explorer
>       var attr = res.getAttributeNames()
>       for (var i = 0; i < attr.length; i++)
>         res.removeAttribute(attr[i])
>       attr = o.getAttributeNames()
>       for (i = 0; i < attr.length; i++)
>         res.setAttribute(attr[i],o.getAttribute(attr[i]))
> */
>       console.log("cloned "+o.tagName+" from "+index)
> /*      console.log(attr) */
>       state.oList.push(o)
>       state.cList.push(res)
>       state.nList.push(index)
>       return res
>     }
>   } else if (this.inPrototype(o,"clone") && typeof(proto.clone) == "function") {
>     return o.clone()
>   }
> 
>   try
>   {
>     // Simulate Object.create(Object.getPrototypeOf(o)) because Object.create will appear to
>     // work, but for some classes, the resulting object will fail in unpredictable ways. (A few
>     // examples of classes that fail: BarProp, History, MimeType, Plugin)
>     res = eval("new "+name+"()")
>     console.log("=> new "+name+"()")
>   } catch(e) {
>     res = {}
>     console.log("=> simulate "+name)
>   }
>   state.oList.push(o)
>   state.cList.push(res)
>   state.nList.push(index)
>   for (i in o)
>     try // debugging
>     {
>       if (name == "HTMLDocument" && i.match(/^file\w+Date$/))
>         continue // avoid bug in Internet Explorer
>       var oi = o[i] // debugging
>       if (!this.isDeprecated(o,i)
>        && !(o[i] instanceof Event) // avoid bug in Internet Explorer
>        && !(o[i] instanceof DOMStringMap) // avoid bud in Internet Explorer
>        && (!(i in res) || o.hasOwnProperty(i)))
>       { // added
>         console.log("descend "+index+"."+i+" ["+whatis(o[i])+"]")
>         try // debugging
>         {
>           res[i] = this.__clone_deep__(o[i],state,index+"."+i,depth+1)
>         } catch(e) { alert(this.formatError(e,":include-stack:")); debugger }
>         console.log("cloned "+index+"."+i)
>       } // added
>     } catch(e) { alert(this.formatError(e,":include-stack:")); debugger }
>   if (name != this.className(res))
>     Object.defineProperty(res,"__clone_of__",{value: name, enumerable: false, writable: false})
>   return res
481a598,611
> /* ********** constructorOf *********************************************************************
> function constructorOf(o)
> 
> Return constructor of an object
> */
> 
> Exelana_utility.prototype.constructorOf = function(o)
> {
>   try
>   {
>     return "constructor" in o ? o.constructor : null
>   } catch(e) { return null }
> }
> 
514a645,646
>     if (action == null)
>       action = "stop"
586c718
<   } catch(e) { this.complain(e.formatError()) }
---
>   } catch(e) { this.complain(this.formatError(e)) }
667c799,801
<       names.push(i)
---
>       // Ignore deprecated properties
>       if (!this.isDeprecated(ep.obj,i))
>         names.push(i)
680,683c814,816
<     // Stop complaints from Chrome about deprecated properties
<     var deprecated = false
<     if (names[i] == "webkitStorageInfo") deprecated = true
<     if (!deprecated)
---
>     var o = {}
>     var v = ""
>     try
685,686c818,823
<       var o = {}
<       var v = ""
---
>       o = ep.obj[names[i]]
> 
>       if (ep.primitive && (!this.isPrimitive(o) || o == null))
>         continue
>     } catch(e) {
>       v = this.formatError(e)
689c826,831
<         o = ep.obj[names[i]]
---
>         if (typeof(ep.obj[names[i]]) == "object"
>          || typeof(ep.obj[names[i]]) == "unknown")
>           v = "[object] -> "+((e.description) != "" ? e.description : this.formatError(e))
>       } catch(e) {}
>       v = v.replace(/\n/g, "<br>")
>     }
691,694c833,845
<         if (ep.primitive && (!this.isPrimitive(o) || o == null))
<           continue
<       } catch(e) {
<         v = e.formatError()
---
>     var fn = false
>     var prop = {}
>     prop.name = names[i]
>     prop.value = v
>     prop.what = ""
>     try
>     {
>       var too = typeof(o)
>       var cn = this.className(o)
>       var isNull = o == null
>       if (too == "undefined")
>       {
>         // Bug in Chrome for document.all
697,699c848,854
<           if (typeof(ep.obj[names[i]]) == "object"
<            || typeof(ep.obj[names[i]]) == "unknown")
<             v = "[object] -> "+((e.description) != "" ? e.description : e.formatError())
---
>           var m = o.toString().match(/^\[object ([^\]]*)\]$/);
>           if (m && m[1])
>           {
>             too = "object"
>             cn = m[1]
>             isNull = false
>           }
701d855
<         v = v.replace(/\n/g, "<br>")
703,709c857
< 
<       var fn = false
<       var prop = {}
<       prop.name = names[i]
<       prop.value = v
<       prop.what = ""
<       try
---
>       if (too == "function")
711,717c859,869
<         if (typeof(o) == "function")
<           prop.what = "function"
<         else
<           prop.what = this.whatis(o)
<         if (v != "")
<         {
<           var doNothing = null; // Error obtaining the value of the object so no further processing
---
>         prop.what = "function"
>       } else if (isNull) {
>         prop.what = "null"
>       } else if (too == "object") {
>         prop.what = cn == null ? "object" : cn
>       } else {
>         prop.what = this.whatis(o)
>       }
>       if (v != "")
>       {
>         var doNothing = null; // Error obtaining the value of the object so no further processing
719,720c871,872
<         } else if (prop.what == "null") {
<           prop.value = ((ep.web) ? "<i>" : "") + "null" + ((ep.web) ? "</i>" : "")
---
>       } else if (prop.what == "null") {
>         prop.value = ((ep.web) ? "<i>" : "") + "null" + ((ep.web) ? "</i>" : "")
722,730c874,882
<         } else if (prop.what == "function") {
<           fn = true
< 
<         } else if (prop.what == "array") {
<           if (ep.web && o.length > 0)
<             prop.value = "<a href='javascript:descend(\"" +
<               prop.name + "\")'>[] (length "+o.length+")</a>"
<           else
<             prop.value = "[] (length "+o.length+")"
---
>       } else if (prop.what == "function") {
>         fn = true
> 
>       } else if (prop.what == "Array") {
>         if (ep.web && o.length > 0)
>           prop.value = "<a href='javascript:descend(\"" +
>             prop.name + "\")'>[] (length "+o.length+")</a>"
>         else
>           prop.value = "[] (length "+o.length+")"
731a884,904
>       } else if (prop.what == "string") {
>         if (!ep.web || o.length <= 197)
>         {
>           prop.value = this.asHTML(this.asString(o))
>         } else {
>           prop.value = this.asHTML(this.asString(o.substr(0,197)+"..."))
>           prop.what = "<a href='javascript:descend(\"" + prop.name + "\")'>"+prop.what+"</a>"
>         }
>       } else {
>         if (too == "object")
>         {
>           var extra = ""
>           if (o instanceof Attr && "name" in o && o[name] != "")
>             extra = " name="+this.stringify(o.name)
>           else if (o instanceof HTMLDivElement && "id" in o && o.id != "")
>             extra = " id="+this.stringify(o.id)
>           else if (o instanceof HTMLElement && "name" in o && o.name != "")
>             extra = " name="+this.stringify(o.name)
>           else if (o instanceof Text)
>             extra = " "+this.stringify(o.wholeText,40)
>           prop.value = "[object"+extra+"]"
734,735c907,909
<           var canDescend = false
<           var simple = ep.simple
---
>         }
>         var canDescend = false
>         var simple = ep.simple
737,739c911,915
<           if (ep.brief && prop.value.length > 200)
<             prop.value = prop.value.substr(0,197) + "..."
<           if (typeof(o) == "object" && !ep.simple)
---
>         if (ep.brief && prop.value.length > 200)
>           prop.value = prop.value.substr(0,197) + "..."
>         if (too == "object" && !isNull && !ep.simple)
>         {
>           try
741c917
<             try
---
>             if (/^function\([^\)]*\)/.test(prop.value))
743,751c919,925
<               if (/^function\([^\)]*\)/.test(prop.value))
<               {
<                 // I'm no longer sure why this code exists. I've tried to trigger the code without
<                 // success. While it may be a useless remnant, I'm leaving it here just in case
<                 // there is some edge case I didn't consider while trying to trigger it.
<                 this.complain("Hit the condition Tony wanted to test with \""+i+"\"")
<                 fn = true
<               }
<             } catch(e) {}
---
>               // I'm no longer sure why this code exists. I've tried to trigger the code without
>               // success. While it may be a useless remnant, I'm leaving it here just in case
>               // there is some edge case I didn't consider while trying to trigger it.
>               this.complain("Hit the condition Tony wanted to test with \""+i+"\"")
>               fn = true
>             }
>           } catch(e) {}
753c927,929
<             if (!fn)
---
>           if (!fn)
>           {
>             if (ep.what.toLowerCase() == "window" && prop.what.toLowerCase() == "window")
755c931
<               if (ep.what.toLowerCase() == "window" && prop.what.toLowerCase() == "window")
---
>               if (o == obj)
757,766c933,937
<                 if (prop.value == obj)
<                 {
<                   prop.value = "this "+this.asString(o)
<                   simple = true
<                 } else if (prop.value == obj.top) {
<                   prop.value = this.asString(o)+" -> top"
<                   simple = true
<                 } else {
<                   canDescend = true
<                 }
---
>                 prop.value = "this "+prop.value
>                 simple = true
>               } else if (o == obj.top) {
>                 prop.value = prop.value+" -> top"
>                 simple = true
769a941,942
>             } else {
>               canDescend = true
771a945
>         }
773c947,949
<           if (!fn)
---
>         if (!fn)
>         {
>           if (ep.web)
775,784c951,954
<             if (ep.web)
<             {
<               if (prop.value)
<                 prop.value = prop.value.toString().toHTML()
<               if (prop.what == "string")
<                 prop.value = prop.value.replace(/\n/g, "<br>")
< 
<               if (canDescend)
<                 prop.value = "<a href='javascript:descend(\"" + prop.name + "\")'>"+prop.value+"</a>"
<             }
---
>             if (prop.value)
>               prop.value = this.asHTML(prop.value.toString())
>             if (prop.what == "string")
>               prop.value = prop.value.replace(/\n/g, "<br>")
786,787c956,957
<             if (typeof(o) == "object" && !simple)
<               prop.value += this.__enumerate_children__(o,objName+"['"+prop.name+"']")
---
>             if (canDescend)
>               prop.value = "<a href='javascript:descend(\"" + prop.name + "\")'>"+prop.value+"</a>"
788a959,961
> 
>           if (too == "object" && !simple)
>             prop.value += this.__enumerate_children__(o,objName+"['"+prop.name+"']")
790,792d962
<       } catch(e) {
<         prop.value = "caught "+e.formatError()
<         this.debugThrow(e, "enumerateProperties: format the value")
793a964,968
>     } catch(e) {
>       prop.value = "caught "+this.formatError(e)
>       if (this.debugging)
>         this.debugThrow(e, "enumerateProperties: format the value")
>     }
795c970,972
<       if (fn)
---
>     if (fn)
>     {
>       try
797,811c974,976
<         try
<         {
<           prop.value = this.asString(o)
<         } catch(e) {
<           prop.value = e.formatError()
<         }
<         ep.functions.push(prop)
< 
<       } else {
<         try
<         {
<           if (ep.own && !ep.obj.hasOwnProperty(prop.name))
<             continue
<         } catch(e) {}
<         ep.properties.push(prop)
---
>         prop.value = this.asString(o)
>       } catch(e) {
>         prop.value = this.formatError(e)
812a978,985
>       ep.functions.push(prop)
>     } else {
>       try
>       {
>         if (ep.own && !ep.obj.hasOwnProperty(prop.name))
>           continue
>       } catch(e) {}
>       ep.properties.push(prop)
814a988,989
>   if (typeof(obj) == "object" && obj != null && "__clone_of__" in obj)
>     ep.what = "clone of "+obj.__clone_of__
828,830c1003
<       // Stop complaints from Chrome about deprecated properties
<       if (what == "document" && x == "origin") next
<       if (x == "webkitStorageInfo") next
---
>       if (this.isDeprecated(obj,x)) next
863c1036
<       return " ("+p.length+" children; "+fnOut+")"
---
>       return " ("+p.length+(p.length == 1 ? " child; " : " children; ")+fnOut+")"
865c1038
<       return " ("+p.length+" children)"
---
>       return " ("+p.length+(p.length == 1 ? " child) ": " children)")
873a1047,1105
> /* ********** getBrowser ************************************************************************
> getBrowser()
> 
> b = getBrowser();re = new RegExp("\\b"+b+"/([\\d+\\.]*)\\b")
> 
> Returns the browser
> */
> Exelana_utility.prototype.getBrowser = function()
> {
>   if (!("browser" in this))
>   {
>     var bt = this.__getBrowserTag__()
>     var browsers = { CriOS: "Chrome", FBAV: "Facebook", FxiOS: "Firefox",
>       MSIE: "InternetExplorer", OPR: "Opera", Trident: "InternetExplorer" }
>     if (bt in browsers)
>       return this.browser = browsers[bt]
>     return this.browser = bt
>   }
>   return this.defined(this.browser) ? this.browser: ""
> }
> 
> /* ********** __getBrowserTag__ *****************************************************************
> __getBrowserTag__()
> 
> Returns the tag used to identify the browser in the user agent string
> */
> Exelana_utility.prototype.__getBrowserTag__ = function()
> {
>   if (!("browserTag" in this))
>   {
>     var browsers = { Edge: /\bEdge\//, Chromium: /\bChromium\/\b/, Chrome: /\bChrome\//,
>       CriOS: /\bCriOS\//, SeaMonkey: /\bSeaMonkey\//, Firefox: /\bFirefox\//, FxiOS: /\bFxiOS\//,
>       Safari: /\bSafari\//, MSIE: /\bMSIE\b/, Trident: /\bTrident\//, Opera: /\bOpera\//,
>       OPR: /\bOPR\//, FBAV: /\bFBAV\// }
>     for (var i in browsers)
>       if (navigator.userAgent.match(browsers[i]))
>         return this.browser = i
>   }
>   return this.defined(this.browserTag) ? this.browserTag: ""
> }
> 
> /* ********** getBrowserVersion *****************************************************************
> getBrowserVersion()
> 
> Returns the browser version
> */
> Exelana_utility.prototype.getBrowserVersion = function()
> {
>   if (!("browserVersion" in this))
>   {
>     var browsers = { Trident: /\brv:([a-zA-Z\d\.]+)/ }
>     var bt = this.__getBrowserTag__()
>     var re = bt in browsers ? browsers[bt] : new RegExp("\\b"+bt+"/([a-zA-Z\\d+\\.]*)\\b")
>     var m = navigator.userAgent.match(re)
>     return this.browserVersion = m[1]
>   }
>   return this.browserVersion
> }
> 
898a1131,1185
> /* ********** getEngine *************************************************************************
> getEngine()
> 
> Returns the rendering engine
> */
> Exelana_utility.prototype.getEngine = function()
> {
>   if (!("engine" in this))
>   {
>     var engines = { Gecko: /\bGecko\/[\d\.]*\b/, WebKit: /\bAppleWebKit\/[\d\.]*\b/,
>       Presto: /\bOpera\/[\d\.]*\b/, Trident: /\bTrident\/[\d\.]*\b/,
>       Blink: /\bChrome\/[\d\.]*\b/ }
>     for (var i in engines)
>       if (navigator.userAgent.match(engines[i]))
>         return this.engine = i
>   }
>   return this.defined(this.engine) ? this.engine : ""
> }
> 
> /* ********** hierarchy *************************************************************************
> object hierarchy(o)
> 
> Return array containing the object hierarchy of the object
> */
> 
> Exelana_utility.prototype.hierarchy = function(o)
> {
>   var list = []
>   try
>   {
>     if (typeof(o) == "function" && this.prototypeOf(o) != null)
>     {
>       list.push(this.className(o.prototype))
>       o = o.prototype
>     }
>     while ((o = Object.getPrototypeOf(o)) != null)
>       list.push(this.className(o))
>   } catch(e) {}
>   return list
> }
> 
> /* ********** inPrototype ***********************************************************************
> object inPrototype(o,key)
> 
> Return true if prototype of an object contains the key
> */
> 
> Exelana_utility.prototype.inPrototype = function(o,key)
> {
>   try
>   {
>     return key in Object.getPrototypeOf(o)
>   } catch(e) { return false }
> }
> 
909a1197,1209
> /* ********** isDeprecated **********************************************************************
> boolean isDeprecated(object,value)
> 
> Returns true if usage of object.value is deprecated
> */
> 
> Exelana_utility.prototype.isDeprecated = function(o,value)
> {
>   if (o == document && value == "origin") return true
>   if (value == "webkitStorageInfo") return true
>   return false
> }
> 
933a1234,1263
> /* ********** propagateErrors *******************************************************************
> boolean propagateErrors([flag])
> 
> Turn error propagatation on or off
> 
> When error propagatation is on, formatError will throw the error rather than formatting it, which
> allows higher level code to intercept the error instead.
> */
> 
> Exelana_utility.prototype.propagateErrors = function(flag)
> {
>   var previous = this.propagate
>   this.propagate = (flag === undefined) ? true : flag
>   return previous
> }
> 
> /* ********** prototypeOf ***********************************************************************
> object prototypeOf(o)
> 
> Return prototype of an object
> */
> 
> Exelana_utility.prototype.prototypeOf = function(o)
> {
>   try
>   {
>     return (typeof(o) == "object" || typeof(o) == "function") ? Object.getPrototypeOf(o) : null
>   } catch(e) { return null }
> }
> 
984c1314
<   } catch(e) { this.complain(e.formatError()) }
---
>   } catch(e) { this.complain(this.formatError(e)) }
1051c1381
<   } catch(e) { this.complain(e.formatError()) }
---
>   } catch(e) { this.complain(this.formatError(e)) }
1126c1456
<         if (!settings.raw) body = body.toHTML()
---
>         if (!settings.raw) body = this.asHTML(body)
1142c1472
<     var m = "showInWindow "+where+": "+e.formatError()
---
>     var m = "showInWindow "+where+": "+this.formatError(e)
1154,1155d1483
< ************* showProperties ********************************************************************
< ************* showProps *************************************************************************
1158,1159d1485
< Object showProperties(obj, limits)
< Object showProps(obj, limits)
1179c1505
< Exelana_utility.prototype.showNamedProperties = function(obj,objName,limits="")
---
> Exelana_utility.prototype.showNamedProperties = function(obj,objName,limits)
1180a1507
>   limits = (limits === undefined) ? "" : limits
1188,1197d1514
< Exelana_utility.prototype.showProperties = function(objName,limits="")
< {
<   return this.showNamedProperties(objName,objName,limits)
< }
< 
< Exelana_utility.prototype.showProps = function(objName,limits="")
< {
<   return this.showNamedProperties(objName,objName,limits)
< }
< 
1200a1518
>   var settings = {}
1203,1204c1521,1529
<     head: ["<"+"script>",
<            "function descend(nm)",
---
>     head: ["<style>",
>            "  h4 { margin-block-end: 0px; ",
>            "  .element { width: 100%; white-space: nowrap; overflow: hidden; "+
>                  "text-overflow: ellipsis; }",
>            "  .stringify { width: 100%; white-space: nowrap; overflow: hidden; "+
>                  "text-overflow: ellipsis; }",
>            "</style>",
>            "<"+"script>",
>            "function descend(nm,run)",
1208,1209c1533,1544
<            "    window.utility.__descendProps__(window, nm)",
<            "  } catch(e) { window.utility.complain(window.utility.formatError(e)) }",
---
>            "    run = (run === undefined) ? false : run",
>            "    window.utility.__descendProps__(window,nm,run)",
>            "  } catch(e) { this.complain(window.utility.formatError(e)) }",
>            "}",
>            "function show(id)",
>            "{",
>            "  try",
>            "  {",
>            "    var oElem = document.getElementById(id)",
>            "    oElem.nextElementSibling.style.display = ''",
>            "    oElem.style.display = 'none'",
>            "  } catch(e) {}",
1232c1567
<   out += this.__renderProps__(sp,sp.desc)
---
>   out += this.__renderProps__(sp,sp.title)
1237c1572
< /* private */ Exelana_utility.prototype.__descendProps__ = function(w, name)
---
> /* private */ Exelana_utility.prototype.__descendProps__ = function(w,name,run)
1240a1576
>     run = (run === undefined) ? false : run
1251c1587
<     if (divId in root.children)
---
>     if (divId in root.children && !run)
1260a1597,1601
>       if (divId in root.children)
>       {
>         div = w.document.getElementById(divId)
>         div.parentElement.removeChild(div)
>       }
1264c1605,1614
<       sp = this.enumerateProperties(dp.obj[name],dp.name+"."+name,limits.join(","))
---
>       var obj = dp.obj[name]
>       if (run)
>       {
>         try
>         {
>           obj = obj()
> //        } catch(e) { dp.w.complain(this.formatError(e)) }
>         } catch(e) { obj = this.formatError(e,":suppress-caller:") }
>       }
>       sp = this.enumerateProperties(obj,dp.name+"."+name,limits.join(","))
1266a1617,1620
>       var what = sp.what
>       for (var i = 0; i < dp.properties.length; i++)
>         if (dp.properties[i].name == name)
>           what = dp.properties[i].what
1268c1622
<         dp.name + "</a>." + name + " ("+sp.what+")"
---
>         dp.name + "</a>." + name + (run ? "()" : "") + " ("+what+")"
1286c1640
<   } catch(e) { this.complain(e.formatError()) }
---
>   } catch(e) { this.complain(this.formatError(e)) }
1308a1663,1676
>   if (sp.what == "string")
>   {
>     out += "<pre class='element'>"+this.asHTML(sp.obj)+"</pre>"
>     return out
>   }
> 
>   if (sp.obj != null && sp.obj instanceof HTMLElement)
>   {
>     var html = sp.obj.outerHTML.substr(0,200)
>     out += "<pre class='element'>"+this.asHTML(html)+
>       (sp.obj.outerHTML.length > 200 ? "<a id='ellipsis' href='javascript:show(\"ellipsis\")'>...</a>"+
>        "<span style='display:none'>"+this.asHTML(sp.obj.outerHTML.substr(200))+"</span>" : "")+
>       "</pre>"
>   }
1310a1679,1683
>   else if (sp.obj != null)
>     out += "<div class='stringify'>"+
>         this.stringify(sp.obj,window.outerWidth/8.5)+
>       "</div><br>\n";
> 
1333c1706
<             e.formatError() + "</td></tr>"
---
>             this.formatError(e) + "</td></tr>"
1349a1723,1731
>         {
>           var fnOut = this.asHTML(sp.functions[i].value)
>           var m = sp.functions[i].value.match(/^([^\n\r]*)/)
>           if (m && m[1].length > 0 && m[1].length < fnOut.length)
>           {
>             var f = "fn_"+sp.functions[i].name
>             fnOut = this.asHTML(m[1])+"<a id='"+f+"' href='javascript:show(\""+f+"\")'>...</a>"+
>               "<span style='display:none'>"+this.asHTML(sp.functions[i].value.substr(m[1].length))+"</span>"
>           }
1351,1353c1733,1735
<             sp.functions[i].name + "</td><td>" +
<             sp.functions[i].value.toHTML() + "</td></tr>"
<         else
---
>             "<a href='javascript:descend(\"" + sp.functions[i].name + "\",true)'>" +
>               sp.functions[i].name + "</a>" + "</td><td><pre>" + fnOut + "</pre></td></tr>"
>         } else {
1355a1738
>         }
1361c1744
<             e.formatError() + "</td></tr>"
---
>             this.formatError(e) + "</td></tr>"
1397c1780
<   } catch(e) { alert(e.formatError()) }
---
>   } catch(e) { alert(this.formatError(e)) }
1400c1783
< Exelana_utility.prototype.snapshotCompare = function(savedAs,compareTo=null)
---
> Exelana_utility.prototype.snapshotCompare = function(savedAs,compareTo)
1403a1787
>     compareTo = (compareTo === undefined) ? null : compareTo
1456c1840
<       } catch(e) { alert(name+": "+e.formatError()) }
---
>       } catch(e) { alert(name+": "+this.formatError(e)) }
1504c1888,2111
<   } catch(e) { this.complain(e.formatError()) }
---
>   } catch(e) { this.complain(this.formatError(e)) }
> }
> 
> /* ********** stringify *************************************************************************
> String stringify(x,[max])
> 
> Returns string describing input (optionally limited to length <max>)
> 
> Things to note:
>   - This function is intended to be used for debugging purposes only and runs very slowly over
>     complicated objects such as an instance of Window.
>   - The output is similar to that of JSON.stringify except that if max is specified, the output
>     will be truncated with one or more ellipses to fit within the specified number of characters.
>   - If the input is an instance of Node (such as HTMLElement) or contains any instances of Node,
>     the output will not include certain child elements that are instances of Node (such as
>     "parentNode" or "nextSibling"). Otherwise, the linked list nature of DOM nodes would cause
>     the procedure to run forever.
>   - If the input contains references to window, document, or document.body, the output will
>     simply contain the name of the respective object.
> */
> Exelana_utility.prototype.stringify = function(x,max,state,name)
> {
>   try
>   {
>     max = (max === undefined) ? -1 : max
>     state = (state === undefined) ? null : state
>     name = (name === undefined) ? null : name
>     if (state != null && !state.done && state.oList.length >= 100)
>     {
>       // console.log("stringify: exiting from deep recursion")
>       // console.log(state)
>       state.done = true
>     }
>     if (state != null && state.done)
>       return ""
> 
>     if (x == null)
>     {
>       if (max < 0 || max >= 4)
>       {
>         return "null"
>       } else {
>         if (state != null) state.done = true
>         return (max >= 3) ? "..." : ""
>       }
>     }
> 
>     var i,v
>     var tx = typeof(x)
>     if (tx == "string")
>     {
>       v = x.replace(/\"/g, "\\\"")
>       if (max >= 0 && v.length > max - 2)
>         v = "\"" + ((max <= 5) ? "..." : v.substr(0,max-5)+"...") + "\""
>       else
>         v = "\"" + v + "\""
>       if (max >= 0 && v.length > max)
>       {
>         v = (max >= 3) ? "..." : ""
>         if (state != null) state.done = true
>       }
>       return v
>     } else if (tx != "object") {
>       v = x.toString()
>       if (max >= 0 && v.length > max)
>       {
>         v = (max >= 3) ? "..." : ""
>         if (state != null) state.done = true
>       }
>       return v
>     }
> 
>     if (state == null) state = { done: false, oList: [], nList: [] }
>     if (name == null) name = "self"
> 
>     var inner = ""
>     var n
>     if (this.className(x) == "Array")
>     {
>       if (x.length == 0)
>         return (max <= 1) ? "" : "[]"
>       if (max >= 0 && max <= 4)
>       {
>         if (state != null) state.done = true
>         return (max >= 3) ? "..." : ""
>       }
>       if (max >= 0) max -= 2
>       var v = []
>       n = 2
>       for (i = 0; i < x.length; i++)
>       {
>         v[i] = this.stringify(x[i],-1,state,name+"["+i+"]")
>         n += v[i].length + 1
>       }
>       if (max < 0 || n < max)
>       {
>         return "["+v.join(",")+",]"
>       }
> 
>       for (i = 0; i < x.length; i++)
>       {
>         var adjust = ((i < x.length - 1) ? 4 : 1)
>         if (v[i].length <= max - adjust)
>         {
>           inner += v[i]+","
>           max -= v[i].length + 1
>         } else {
>           var short = this.stringify(x[i],max - adjust,state,name+"["+i+"]")
>           if (short != "" && short != "..." && short.length <= max - adjust)
>           {
>             inner += short+(adjust > 1 ? ",..." : ",")
>             state.done = true
>             break
>           } else {
>             inner += "..."
>             state.done = true
>             break
>           }
>         }
>       }
>       return "[" + inner + "]"
>     } else if (x instanceof RegExp) {
>       var re = x.toString()
>       if (max < 0 || re.length <= max)
>         return re
>       if (state != null) state.done = true
>       if (max <= 5)
>       {
>         if (state != null) state.done = true
>         return (max < 3 ? "" : (max < 5 ? "..." : "/.../"))
>       }
>       re = re.substr(max-5,1) == "\\" ? re.substr(0,max-5) : re.substr(0,max-4)
>       return re+".../"
>     } else {
>       if (max >= 0 && max <= 4)
>       {
>         if (state != null) state.done = true
>         return (max >= 3) ? "..." : ""
>       }
>       if (max >= 0) max -= 2
>       for (i = 0; i < state.oList.length; i++)
>         if (state.oList[i] === x)
>           return state.nList[i]
>       state.oList.push(x)
>       state.nList.push(name)
>       if (name == "SELF")
>       {
>         state.oList.push(window)
>         state.nList.push("window")
>         state.oList.push(document)
>         state.nList.push("document")
>         state.oList.push(document.body)
>         state.nList.push("document.body")
>       }
> 
>       var ix = []
>       var label = {}
>       var names = {}
>       var v = {}
>       n = 2
>       for (i in x)
>       {
>         if (this.isDeprecated(x,i)
>          || (x instanceof Node && x[i] instanceof Node)
>          || typeof(x[i]) == "function")
>           continue
> 
>         ix.push(i)
>         if (i.match(/^\d+[^\d]/))
>         {
>           label[i] = "\"" + i + "\""
>           names[i] = name+"["+label[i]+"]"
>         } else {
>           label[i] = i
>           names[i] = name+"."+i
>         }
>         v[i] = this.stringify(x[i],-1,state,names[i])
>         n += label[i].length + v[i].length + 2
>       }
>       if (max < 0 || n < max)
>       {
>         var out = ""
>         for (i = 0; i < ix.length; i++)
>           out += label[ix[i]]+":"+v[ix[i]]+","
>         return "{"+out+"}"
>       }
> 
>       for (i = 0; i < ix.length; i++)
>       {
>         var elem = ix[i]
>         var adjust = ((i < ix.length - 1) ? 4 : 1)
>         if (label[elem].length + v[elem].length + 2 <= max - adjust)
>         {
>           inner += label[elem]+":"+v[elem]+","
>           max -= label[elem].length + v[elem].length + 2
>         } else {
>           var short = this.stringify(x[elem],max - label[elem].length - 1 - adjust,state,names[elem])
>           if (short != "" && short != "..." && label[elem].length + short.length <= max - adjust)
>           {
>             inner += label[elem]+":"+short+(adjust > 1 ? ",..." : ",")
>             state.done = true
>             break
>           } else {
>             inner += "..."
>             state.done = true
>             break
>           }
>         }
>       }
>       return "{" + inner + "}"
>     }
>   } catch(e) {
>     this.formatError(e)
>     if (typeof(x) == "object")
>     {
>       try
>       {
>         return x.toString()
>       } catch(e) {
>         return "[object]"
>       }
>     }
>   }
>   return x
1570c2177
<           m = this.table_array_parent_index(oDefinition,oTable)
---
>           m = this.__table_array_parent_index__(oDefinition,oTable)
1631c2238
<               oTD.appendChild(this.create_table_array(oChildDef,n))
---
>               oTD.appendChild(this.__create_table_array__(oChildDef,n))
1681c2288
<       m = this.table_array_parent_index(oDefinition,oTable)
---
>       m = this.__table_array_parent_index__(oDefinition,oTable)
1754c2361
<   } catch(e) { this.complain(e.formatError()) }
---
>   } catch(e) { this.complain(this.formatError(e)) }
1757c2364
< /* private */ Exelana_utility.prototype.create_table_array = function(oDefinition,n)
---
> /* private */ Exelana_utility.prototype.__create_table_array__ = function(oDefinition,n)
1803c2410
< /* private */ Exelana_utility.prototype.table_array_parent_index = function(oDefinition,oTable)
---
> /* private */ Exelana_utility.prototype.__table_array_parent_index__ = function(oDefinition,oTable)
1819c2426,2427
< /* ********** toggle(elem,styleA,styleB) ********************************************************
---
> /* ********** toggle ****************************************************************************
> toggle(elem,styleA,styleB)
1826d2433
<   theToggle = oElem
1836a2444
>     {
1837a2446,2447
>       console.log("toggle b "+i+" "+bParts[i][0]+":"+bParts[i][1]+" => "+oElem.style[bParts[i][0]])
>     }
1839a2450
>     {
1840a2452,2453
>       console.log("toggle a "+i+" "+aParts[i][0]+":"+aParts[i][1]+" => "+oElem.style[aParts[i][0]])
>     }
1844c2457,2458
< /* ********** whatis(obj) ***********************************************************************
---
> /* ********** whatis ****************************************************************************
> whatis(thing)
1846c2460
< Examines an object and returns a string describing the object's type
---
> Examines something and returns a string describing it
1850c2464
<                     will also return boolean, number, and string respectivley for Boolean,
---
>                     will also return boolean, number, and string respectively for Boolean,
1853,1854c2467,2468
<   Custom objects:   object.id
<   HTML objects:     html.TAG (e.g., html.BODY or HTML.INPUT.button)
---
>   Custom objects:   name of constructor
>   HTML objects:     html.TAG (e.g., html.body or html.input.button)
1858,1861d2471
< A custom object must include an ID in its prototype to be detected; for example
<   function Custom() {}  // constuctor
<   Custom.prototype.id = "Custom"
< 
1869,1871d2478
<     if (x == null)
<       return "null"
< 
1878,1885c2485,2486
<     var con = null
<     var proto = null
<     try
<     {
<       con = "constructor" in x ? x.constructor : null
<       proto = "prototype" in x ? x.prototype : con && "prototype" in con ? con.prototype : null
<     } catch(e) {
<     }
---
>     if (x == null)
>       return "null"
1887c2488
<     if (tx == "object" && con != null)
---
>     if (!(x instanceof HTMLElement))
1889,1894c2490,2495
<       /* This convoluted approach is to simulate the following even when the object was created
<          in another frame/window: Type.prototype.isPrototypeOf(x)
< 
<          For example, if another window evaluated "x = new Array()" then
<          Array.prototype.isPrototypeOf(x) returns true in that window, but false in another
<          window because each window has its own Array constructor.
---
>       var name = this.className(x)
>       switch (name)
>       {
>         case null:
>         case "Object":
>           break // continue processing below
1896,1898c2497,2498
<          By converting the constructor to a string and looking for
<          "function Array(){[native code]}", we can determine that it is an array regardless of
<          whether it was created locally or remotely. */
---
>         case "HTMLDocument":
>           return "document"
1900,1910c2500,2501
<       var m = con.toString().
<         match(/^\s*function\s+(\w+)\s*\(\s*\)\s*\{\s*\[native code\]\s*\}\s*$/)
<       if (m && m[1])
<       {
<         var complex = ["array", "boolean", "date", "error", "function", "number", "regexp",
<                        "string"]
<         for (var c in complex)
<         {
<           if (m[1].toLowerCase() == complex[c])
<            return complex[c]
<         }
---
>         default:
>           return name.toLowerCase()
1913c2504
< 
---
> 
1914a2506
>     var proto = this.prototypeOf(x)
1983,1984c2575,2576
<     return tx
<   } catch(e) { this.complain(e.formatError()) }
---
>     return tx.toLowerCase()
>   } catch(e) { this.complain(this.formatError(e)) }
2277c2869
< function Exelana_console(dest)
---
> function Exelana_console(dest,utility)
2278a2871
>   this.utility = utility
2281c2874
<     this.destName = this.whatis(dest)
---
>     this.destName = this.utility.whatis(dest)
2533a3127
>   if (settings === undefined) settings = {}
2536c3130
<   if (this.settings.debugCreate)
---
>   if ("debugCreate" in settings)
2652c3246
<     this.console = new Exelana_console(this.form.console)
---
>     this.console = new Exelana_console(this.form.console,this.utility)
2682c3276,3277
<     this.window.document.body.removeChild(this.div)
---
>     this.div.innerHTML = ""
>     this.div.parentElement.removeChild(this.div)
2805a3401
>   var p = this.utility.propagateErrors(false)
2809c3405
<     this.console.print(e.formatError())
---
>     this.console.print(this.utility.formatError(e))
2812c3408
<     this.form.output.value += e.formatError()
---
>     this.form.output.value += this.utility.formatError(e)
2814c3410
<     this.utility.complain("Exelana_evaluator internal error\n"+e.formatError())
---
>     this.utility.complain("Exelana_evaluator internal error\n"+this.utility.formatError(e))
2815a3412
>   this.utility.propagateErrors(p)
2820c3417
< // The following functions handle keypresses and button clicks in the debugger
---
> // The following functions handle keypresses and button clicks in the Evaluator
2924a3522
>   var p = this.utility.propagateErrors(false)
2925a3524
>   this.utility.propagateErrors(p)
2930a3530,3531
>       __output__ = this.utility.stringify(__eval_v__)
> /* Old (hopefully obsolete) version
2946a3548
> */
2979c3581,3583
<       this.form.output.value = e.formatError(":suppress-location:")
---
>       var p = this.utility.propagateErrors(false)
>       this.form.output.value = this.utility.formatError(e,":suppress-location:")
>       this.utility.propagateErrors(p)
3019c3623
<     body: ["<h3>Results of "+this.lastInput.toHTML()+
---
>     body: ["<h3>Results of "+this.utility.asHTML(this.lastInput)+
3021c3625
<            this.form.output.value.toHTML()],
---
>            this.utility.asHTML(this.form.output.value)],
