diff --git a/blog/2014/10/26/README.md b/blog/2014/10/26/README.md index 7078a7389..e44c17a50 100644 --- a/blog/2014/10/26/README.md +++ b/blog/2014/10/26/README.md @@ -1,7 +1,6 @@ JavaScript Negativity --- -For those of us coming from the world of C, it's easy to be "negative" about the way JavaScript deals with 32-bit -integers. +Coming from the C programming language, it's easy to be "negative" about how JavaScript deals with 32-bit integers. As a newcomer, you quickly learn that JavaScript supports only one numeric data type -- 64-bit floats -- and you groan. @@ -9,9 +8,9 @@ Then you learn that all the "bitwise" operators (**~**, **|**, **&**, **^**, **& **>>>**) treat their operands as 32-bit integer values and produce 32-bit integer results, and you breathe a sigh of relief. -But then you start noticing oddities. In 32-bit C programming, you can take any 32-bit value, -such as -1526726656 (which is equivalent to 0xA5000000), mask it with 0x80808080, and get 0x80000000. However, -in JavaScript, you actually get -0x80000000, which, sadly, is not equal to 0x80000000. +But then you start noticing oddities. In C, you can take any 32-bit value, such as -1526726656 (which is equivalent +to 0xA5000000), mask it with 0x80808080, and get 0x80000000. However, in JavaScript, you actually get -0x80000000, +which, sadly, is not equal to 0x80000000. To verify, type the following into any JavaScript REPL (eg, Node): @@ -22,28 +21,28 @@ To verify, type the following into any JavaScript REPL (eg, Node): > n.toString(16) '-80000000' -So the notion that bitwise operators yield 32-bit results isn't exactly right; every result continues to be -sign-extended into the entire 52 "significand" bits of the underlying 64-bit float. And it's impossible to simply -"mask away" those unwanted sign bits, thanks to the fundamental restriction of JavaScript bitwise operators: -they operate *only* on the low 32 bits. +So the notion that bitwise operators yield 32-bit results isn't exactly right; the sign (bit 31) of every 32-bit +result is always extended into the entire 52 "significand" bits of the underlying 64-bit float. And it's +impossible to simply "mask away" those additional sign bits, thanks to the fundamental restriction of JavaScript bitwise +operators: they operate *only* on the low 32 bits. -If you really want 0x80000000 instead of -0x80000000, add 0x100000000: +The easiest way to remove the high-order sign bits from a negative 32-bit value is to add 0x100000000: > n = (n < 0? n + 0x100000000 : n) 2147483648 > n.toString(16) '80000000' -This works because JavaScript is perfectly capable of representing 0x80000000 as a positive number. -But be careful, because as soon as you perform *any* bitwise operation on a value with bit 31 set, even -something as innocuous-looking as: +This works because JavaScript is perfectly capable of representing 0x80000000, or any other 32-bit value, as a positive +number. But be careful, because as soon as you perform *any* bitwise operation on a value with bit 31 set, even +an operation as innocuous-looking as: > n |= 0 -2147483648 > n.toString(16) '-80000000' -Viola: instant negative number! To continue the fun, now "or" a one into bit 0: +Viola: instant negative number! To continue the fun, now "or" one into bit 0: > n |= 1 -2147483647 @@ -53,7 +52,7 @@ Viola: instant negative number! To continue the fun, now "or" a one into bit 0: Viola: all low 32 bits have instantly flipped! Actually, no, this time, I'm pulling your leg. The low 32 bits of the internal value are exactly what you would -expect: 0x80000001 (the internal representation would look more like 0xFFFFF80000001). The toString() method is +expect: 0x80000001 (the internal representation looks more like 0xFFFFF80000001). The toString() method is just a little misleading. As the MDN docs explain, for a negative number, toString() returns the positive representation of the number, preceded by a - sign, *not* the "two's complement" of the number. diff --git a/my_modules/pcjs-client/lib/state.js b/my_modules/pcjs-client/lib/state.js index a6e5b53c3..07dba7615 100644 --- a/my_modules/pcjs-client/lib/state.js +++ b/my_modules/pcjs-client/lib/state.js @@ -33,6 +33,7 @@ "use strict"; if (typeof module !== 'undefined') { + var web = require("./../../shared/lib/weblib"); var Component = require("./../../shared/lib/component"); } @@ -81,25 +82,6 @@ State.key = function(component, sVersion, sSuffix) { return key; }; -/** - * State.localStorage() - * - * TODO: State's localStorage calls should probably be moved to wrappers in weblib.js, for consistency. - * - * @return {boolean} true if localStorage available, false if not - */ -State.localStorage = function() { - try { - /* - * A try/catch block is required, because if the user has disabled localStorage, some browsers feel the need - * to throw an exception on any attempt to access it, even when using "typeof". - */ - return typeof window.localStorage !== 'undefined'; - } catch(e) { - return false; - } -}; - /** * State.compress(aSrc) * @@ -269,7 +251,7 @@ State.prototype = { * @return {boolean} true if state exists in localStorage, false if not * * WARNING: Make sure you follow this call with either a call to parse() or unload(), - * because any stringified data we've loaded isn't usable until it's been parsed. + * because any stringified data that we've loaded isn't usable until it's been parsed. */ load: function(s) { if (s) { @@ -283,8 +265,8 @@ State.prototype = { */ return true; } - if (State.localStorage()) { - s = window.localStorage.getItem(this.key); + if (web.hasLocalStorage()) { + s = web.getLocalStorageItem(this.key); if (s) { this[this.id] = s; this.fLoaded = true; @@ -322,19 +304,17 @@ State.prototype = { */ store: function() { var fSuccess = true; - if (State.localStorage()) { + if (web.hasLocalStorage()) { var s = JSON.stringify(this[this.id]); - try { - window.localStorage.setItem(this.key, s); + if (web.setLocalStorageItem(this.key, s)) { if (DEBUG) this.messageDebugger("localStorage(" + this.key + "): " + s.length + " bytes stored"); - } catch (e) { + } else { /* * WARNING: Because browsers tend to disable all alerts() during an "unload" operation, * it's unlikely anyone will ever see the "quota" errors that occur at this point. Need to * think of some way to notify the user that there's a problem, and offer a way of cleaning * up old states. */ - Component.log(e.message || e, "error"); Component.error("Unable to store " + s.length + " bytes in browser local storage"); fSuccess = false; } @@ -380,15 +360,14 @@ State.prototype = { */ clear: function(fAll) { this.unload(); - if (State.localStorage()) { - var i = 0; - while (i < window.localStorage.length) { - var key = window.localStorage.key(i++); - if (key && (fAll || key.substr(0, this.key.length) == this.key)) { - window.localStorage.removeItem(key); - if (DEBUG) this.messageDebugger("localStorage(" + key + ") removed"); - i = 0; - } + var aKeys = web.getLocalStorageKeys(); + for (var i = 0; i < aKeys.length; i++) { + var sKey = aKeys[i]; + if (sKey && (fAll || sKey.substr(0, this.key.length) == this.key)) { + web.removeLocalStorageItem(sKey); + if (DEBUG) this.messageDebugger("localStorage(" + sKey + ") removed"); + aKeys.splice(i, 1); + i = 0; } } }, diff --git a/my_modules/shared/lib/embed.js b/my_modules/shared/lib/embed.js index 2e04b4af3..310e78b89 100644 --- a/my_modules/shared/lib/embed.js +++ b/my_modules/shared/lib/embed.js @@ -41,9 +41,24 @@ if (typeof module !== 'undefined') { var web = require("./weblib"); } +/* + * We now support asynchronous XML and XSL file loads; simply set fAsync (below) to true. + * + * NOTE: For that support to work, we have to keep track of the number of machines on the page + * (ie, how many embedMachine() calls were issued), reduce the count once the XML for each machine + * is fully transformed into HTML, and when the count finally returns to zero, notify all the + * machine component init() handlers. + * + * Also, to prevent those init() handlers from running prematurely, we must disable all page + * notification events at the start of the embedding process (web.enablePageEvents(false)) and + * re-enable them at the end (web.enablePageEvents(true)). + */ +var cMachines = 0; +var fAsync = true; + /** - * loadXML(sFile, idMachine, sStateFile, fResolve) - * + * loadXML(sFile, idMachine, sStateFile, fResolve, done) + * * This is the preferred way to load all XML and XSL files. It uses loadResource() * to load them as strings, which parseXML() can massage before parsing/transforming them. * @@ -51,10 +66,10 @@ if (typeof module !== 'undefined') { * XSL document loaded by JavaScript's XSLT processor, that has prevented me from dynamically * loading any XML machine file that uses the "ref" attribute to refer to and incorporate * another XML document. - * + * * To solve that, I've added an fResolve parameter that tells parseXML() to fetch any * referenced documents ITSELF and insert them into the XML string prior to parsing, instead - * of relying on the XSLT template to pull them in. That fetching is handled by resolveRefs(), + * of relying on the XSLT template to pull them in. That fetching is handled by resolveXML(), * which iterates over the XML until all "refs" have been resolved (including any nested * references). * @@ -66,40 +81,43 @@ if (typeof module !== 'undefined') { * JavaScript XSLT support. Is it broken, is it a security issue, or am I just calling it wrong? * * @param {string} sXMLFile - * @param {string} [idMachine] - * @param {string} [sStateFile] - * @param {boolean} [fResolve] is true to resolve any "ref" attributes - * @return {Array} where [0] contains the unparsed XML string data, and [1] contains a parsed XML object + * @param {string|null|undefined} idMachine + * @param {string|null|undefined} sStateFile + * @param {boolean} fResolve is true to resolve any "ref" attributes + * @param {function(string,Object)} done (string contains the unparsed XML string data, and Object contains a parsed XML object) */ -function loadXML(sXMLFile, idMachine, sStateFile, fResolve) +function loadXML(sXMLFile, idMachine, sStateFile, fResolve, done) { - var response = web.loadResource(sXMLFile); - if (response[0]) { - throw new Error(response[1]); - } - return parseXML(response[1], sXMLFile, idMachine, sStateFile, fResolve); + var doneLoadXML = function(sURLName, sXML, nErrorCode) { + if (nErrorCode) { + done(sXML, null); + return; + } + parseXML(sXML, sXMLFile, idMachine, sStateFile, fResolve, done); + }; + web.loadResource(sXMLFile, fAsync, null, null, doneLoadXML); } /** - * parseXML(sXML, idMachine, sStateFile, fResolve) - * + * parseXML(sXML, sXMLFile, idMachine, sStateFile, fResolve, done) + * * Generates an XML document from an XML string. This function also provides a work-around for XSLT's * lack of support for the document() function (at least on some browsers), by replacing every reference * tag (ie, a tag with a "ref" attribute) with the contents of the referenced file. * - * @param {string|null} sXML + * @param {string} sXML * @param {string|null} sXMLFile - * @param {string} [idMachine] - * @param {string} [sStateFile] - * @param {boolean} [fResolve] is true to resolve any "ref" attributes; default is false - * @return {Array} where [0] contains the unparsed XML string data, and [1] contains a parsed XML object + * @param {string|null|undefined} idMachine + * @param {string|null|undefined} sStateFile + * @param {boolean} fResolve is true to resolve any "ref" attributes; default is false + * @param {function(string,Object)} done (string contains the unparsed XML string data, and Object contains a parsed XML object) */ -function parseXML(sXML, sXMLFile, idMachine, sStateFile, fResolve) +function parseXML(sXML, sXMLFile, idMachine, sStateFile, fResolve, done) { - var xmlDoc = null; - if (sXML) { - if (fResolve) { - sXML = resolveRefs(sXML); + var buildXML = function(sXML, sError) { + if (sError) { + done(sError, null); + return; } if (idMachine) { var sURL = sXMLFile; @@ -110,134 +128,184 @@ function parseXML(sXML, sXMLFile, idMachine, sStateFile, fResolve) * If the resource we requested is not really an XML file (or the file didn't exist and the server simply returned * a message like "Cannot GET /configs/pc/machines/5150/cga/64kb/donkey/index.xml"), we'd like to display a more * meaningful message, because the XML DOM parsers will blithely return a document that contains nothing useful; eg: - * + * * This page contains the following errors:error on line 1 at column 1: * Document is empty Below is a rendering of the page up to the first error. - * + * * Supposedly, the IE XML DOM parser will throw an exception, but I haven't tested that, and unless all other * browsers do that, that's not helpful. - * + * * The best I can do at this stage (assuming web.loadResource() didn't drop any error information on the floor) * is verify that the requested resource "looks like" valid XML (in other words, it begins with a "<"). */ + var xmlDoc = null; if (sXML.indexOf("<") === 0) { - if (window.ActiveXObject || "ActiveXObject" in window) { // second test is required for IE11 on Windows 8.1 - /* - * Another hack for MSIE, which fails to properly load XSL documents containing a tag. - */ - if (!fResolve) { - sXML = sXML.replace(/\s*/g, ""); + try { + if (window.ActiveXObject || "ActiveXObject" in window) { // second test is required for IE11 on Windows 8.1 + /* + * Another hack for MSIE, which fails to properly load XSL documents containing a tag. + */ + if (!fResolve) { + sXML = sXML.replace(/\s*/g, ""); + } + xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM"); + xmlDoc.async = false; + xmlDoc.loadXML(sXML); + } else { + xmlDoc = (new window.DOMParser()).parseFromString(sXML, "text/xml"); } - xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM"); - xmlDoc.async = false; - xmlDoc.loadXML(sXML); - } else { - xmlDoc = (new window.DOMParser()).parseFromString(sXML, "text/xml"); + } catch(e) { + xmlDoc = null; + sXML = e.message; } } else { - throw new Error("unrecognized XML: " + (sXML.length > 255? sXML.substr(0, 255) + "..." : sXML)); + sXML = "unrecognized XML: " + (sXML.length > 255? sXML.substr(0, 255) + "..." : sXML); } + done(sXML, xmlDoc); + }; + if (sXML) { + if (fResolve) { + resolveXML(sXML, buildXML); + return; + } + buildXML(sXML, null); + return; } - return [sXML, xmlDoc]; + done("no data" + (sXMLFile? " for file: " + sXMLFile : ""), null); } /** - * resolvesRefs(sXML) - * + * resolveXML(sXML, done) + * * Replaces every tag with a "ref" attribute with the contents of the corresponding file. - * + * * TODO: Fix some of the limitations of this code, such as: 1) requiring the "ref" attribute * to appear as the tag's first attribute, 2) requiring the "ref" attribute to be double-quoted, - * and 3) requiring the "ref" tag to be self-closing. + * and 3) requiring the "ref" tag to be self-closing. * * @param {string} sXML - * @returns {string} with all tags with "ref" attributes replaced with the referenced file instead + * @param {function(string,(string|null))} done (the first string contains the resolved XML data, the second is for any error message) */ -function resolveRefs(sXML) +function resolveXML(sXML, done) { - var matchRef, sError; + var matchRef; var reRef = /<([a-z]+)\s+ref="(.*?)"(.*?)\/>/g; - while ((matchRef = reRef.exec(sXML))) { + + if ((matchRef = reRef.exec(sXML))) { + var sRefFile = matchRef[2]; - var response = web.loadResource(sRefFile); - var sXMLRef = response[1]; - if (response[0] || !sXMLRef) { - sError = "unable to resolve XML reference: " + matchRef[0] + " (" + response[0] + ")"; - Component.log(sError); - throw new Error(sError); - } - /* - * If there are additional attributes in the "referring" XML tag, we want to insert them - * into the "referred" XML tag; attributes that don't exist in the referred tag should be - * appended, and attributes that DO exist should be overwritten. - */ - var sRefAttrs = matchRef[3]; - if (sRefAttrs) { - var aXMLRefTag = sXMLRef.match(new RegExp("<" + matchRef[1] + "[^>]*>")); - if (aXMLRefTag) { - var sXMLNewTag = aXMLRefTag[0]; - /* - * Iterate over all the attributes in the "referring" XML tag (sRefAttrs) - */ - var matchAttr; - var reAttr = /( [a-z]+=)(['"])(.*?)\2/g; - while ((matchAttr = reAttr.exec(sRefAttrs))) { - if (sXMLNewTag.indexOf(matchAttr[1]) < 0) { - /* - * This is the append case - */ - sXMLNewTag = sXMLNewTag.replace(">", matchAttr[0] + ">"); - } else { - /* - * This is the overwrite case - */ - sXMLNewTag = sXMLNewTag.replace(new RegExp(matchAttr[1] + "(['\"])(.*?)\\1"), matchAttr[0]); - } - } - if (aXMLRefTag[0] != sXMLNewTag) { - sXMLRef = sXMLRef.replace(aXMLRefTag[0], sXMLNewTag); - } - } else { - sError = "missing <" + matchRef[1] + "> in " + sRefFile; - Component.log(sError); - throw new Error(sError); + + var doneReadXML = function(sURLName, sXMLRef, nErrorCode) { + if (nErrorCode || !sXMLRef) { + done(sXML, "unable to resolve XML reference: " + matchRef[0] + " (" + nErrorCode + ")"); + return; + } + /* + * If there are additional attributes in the "referring" XML tag, we want to insert them + * into the "referred" XML tag; attributes that don't exist in the referred tag should be + * appended, and attributes that DO exist should be overwritten. + */ + var sRefAttrs = matchRef[3]; + if (sRefAttrs) { + var aXMLRefTag = sXMLRef.match(new RegExp("<" + matchRef[1] + "[^>]*>")); + if (aXMLRefTag) { + var sXMLNewTag = aXMLRefTag[0]; + /* + * Iterate over all the attributes in the "referring" XML tag (sRefAttrs) + */ + var matchAttr; + var reAttr = /( [a-z]+=)(['"])(.*?)\2/g; + while ((matchAttr = reAttr.exec(sRefAttrs))) { + if (sXMLNewTag.indexOf(matchAttr[1]) < 0) { + /* + * This is the append case + */ + sXMLNewTag = sXMLNewTag.replace(">", matchAttr[0] + ">"); + } else { + /* + * This is the overwrite case + */ + sXMLNewTag = sXMLNewTag.replace(new RegExp(matchAttr[1] + "(['\"])(.*?)\\1"), matchAttr[0]); + } + } + if (aXMLRefTag[0] != sXMLNewTag) { + sXMLRef = sXMLRef.replace(aXMLRefTag[0], sXMLNewTag); + } + } else { + done(sXML, "missing <" + matchRef[1] + "> in " + sRefFile); + return; + } } - } - /* - * Apparently when a Windows Azure server delivers one of my XML files, it may modify the first line: - * - * \n - * - * I didn't determine exactly what it was doing at this point (probably just changing the \n to \r\n), - * but in any case, relaxing the following replace() solved it. - */ - sXMLRef = sXMLRef.replace(/<\?xml[^>]*>[\r\n]*/, ""); + /* + * Apparently when a Windows Azure server delivers one of my XML files, it may modify the first line: + * + * \n + * + * I didn't determine exactly what it was doing at this point (probably just changing the \n to \r\n), + * but in any case, relaxing the following replace() solved it. + */ + sXMLRef = sXMLRef.replace(/<\?xml[^>]*>[\r\n]*/, ""); - sXML = sXML.replace(matchRef[0], sXMLRef); - - reRef.lastIndex = 0; // reset lastIndex, since we just modified the string that reRef is iterating over + sXML = sXML.replace(matchRef[0], sXMLRef); + + resolveXML(sXML, done); + }; + + web.loadResource(sRefFile, fAsync, null, null, doneReadXML); + return; } - return sXML; + done(sXML, null); } /** * embedMachine(sName, sVersion, idElement, sXMLFile, sXSLFile, sStateFile) - * + * * This allows to you embed a machine on a web page, by transforming the machine XML into HTML. * - * @param {string} sName is the app name (eg, "PCjs" or "C1Pjs") - * @param {string} sVersion is the app version (eg, "1.12.1") + * @param {string} sName is the app name (eg, "PCjs") + * @param {string} sVersion is the app version (eg, "1.15.7") * @param {string} idElement * @param {string} sXMLFile - * @param {string} [sXSLFile] + * @param {string} sXSLFile * @param {string} [sStateFile] - * @return {string} containing the complete XML string data, or an error if the XML could not be parsed + * @return {boolean} true if successful, false if error */ function embedMachine(sName, sVersion, idElement, sXMLFile, sXSLFile, sStateFile) { - var sXML = "", sError = "", eMachine = null; - + var eMachine, fSuccess = true; + + cMachines++; + + var doneMachine = function() { + Component.assert(cMachines > 0); + if (--cMachines == 0) { + if (fAsync) web.enablePageEvents(true); + } + }; + + var displayError = function(sError) { + Component.log(sError); + if (eMachine) { + /* + * Our MarkOut module (in convertMDMachineLinks()) creates machine containers that look like this: + * + *
Embedded PC
Embedded PC