/** * @fileoverview The Component class used by all PCjs components. * @author Jeff Parsons * @copyright © Jeff Parsons 2012-2017 * * This file is part of PCjs, a computer emulation software project at . * * PCjs is free software: you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with PCjs. If not, * see . * * You are required to include the above copyright notice in every modified copy of this work * and to display that copyright notice when the software starts running; see COPYRIGHT in * . * * Some PCjs files also attempt to load external resource files, such as character-image files, * ROM files, and disk image files. Those external resource files are not considered part of PCjs * for purposes of the GNU General Public License, and the author does not claim any copyright * as to their contents. */ /* * All PCjs components now use JSDoc types, primarily so that Google's Closure Compiler will compile * everything with zero warnings when ADVANCED_OPTIMIZATIONS are enabled. For more information about * the JSDoc types supported by the Closure Compiler: * * https://developers.google.com/closure/compiler/docs/js-for-compiler#types * * I also attempted to validate this code with JSLint, but it complained too much; eg, it didn't like * "while (true)", a tried and "true" programming convention for decades, and it wanted me to replace * all "++" and "--" operators with "+= 1" and "-= 1", use "(s || '')" instead of "(s? s : '')", etc. * * I prefer sticking with traditional C-style idioms, in part because they are more portable. That * does NOT mean I'm trying to write "portable JavaScript," but some of this code was ported from C code * I'd written long ago, so portability is good, and I'm not going to throw that away if there's no need. * * UPDATE: I've since switched from JSLint to JSHint, which seems to have more reasonable defaults. * And for new code, I have adopted some popular JavaScript idioms, like "(s || '')", although the need * for those kinds of expressions will be reduced as I also start adopting some ES6 features, like * default parameters. */ "use strict"; /** * Since the Closure Compiler treats ES6 classes as @struct rather than @dict by default, * it deters us from defining named properties on our components; eg: * * this['exports'] = {...} * * results in an error: * * Cannot do '[]' access on a struct * * So, in order to define 'exports', we must override the @struct assumption by annotating * the class as @unrestricted (or @dict). Note that this must be done both here and in the * subclass (eg, SerialPort), because otherwise the Compiler won't allow us to *reference* * the named property either. * * TODO: Consider marking ALL our classes unrestricted, because otherwise it forces us to * define every single property the class uses in its constructor, which results in a fair * bit of redundant initialization, since many properties aren't (and don't need to be) fully * initialized until the appropriate init(), reset(), restore(), etc. function is called. * * The upside, however, may be that since the structure of the class is completely defined by * the constructor, JavaScript engines may be able to optimize and run more efficiently. * * @unrestricted */ class Component { /** * Component(type, parms, bitsMessage) * * A Component object requires: * * type: a user-defined type name (eg, "CPU") * * and accepts any or all of the following (parms) properties: * * id: component ID (default is "") * name: component name (default is ""; if blank, toString() will use the type name only) * comment: component comment string (default is undefined) * * Component subclasses will usually have additional (parms) properties. * * @param {string} type * @param {Object} [parms] * @param {number} [bitsMessage] selects message(s) that the component wants to enable (default is 0) */ constructor(type, parms, bitsMessage) { this.type = type; if (!parms) parms = {'id': "", 'name': ""}; this.id = parms['id'] || ""; this.name = parms['name']; this.comment = parms['comment']; this.parms = parms; /* * The following Component properties need to be accessible by other machines and/or command scripts; * well, OK, or we could have exported some new functions to walk the contents of these properties, as we * did with findMachineComponent(), but this works just as well. * * Also, while the double-assignment looks silly (ie, using both dot and bracket property notation), it * resolves a complaint from the Closure Compiler, because if we use ONLY bracket notation here, then the * Compiler wants us to change all the other references to bracket notation as well. */ this.exports = this['exports'] = {}; this.bindings = this['bindings'] = {}; var i = this.id.indexOf('.'); if (i < 0) { this.idComponent = this.id; } else { this.idMachine = this.id.substr(0, i); this.idComponent = this.id.substr(i + 1); } /* * Gather all the various component flags (booleans) into a single "flags" object, and encourage * subclasses to do the same, to reduce the property clutter we have to wade through while debugging. */ this.flags = { ready: false, busy: false, busyCancel: false, powered: false, unloading: false, error: false }; this.fnReady = null; this.clearError(); this.bitsMessage = bitsMessage || 0; /** @type {Object|null} controlPrint is the HTML control, if any, that we can print to */ this.controlPrint = null; this.cmp = null; this.bus = null; this.cpu = null; this.dbg = null; /* * TODO: Consider adding another parameter to the Component() constructor that allows components to tell * us if they support single or multiple instances per machine. For example, there can be multiple SerialPort * components per machine, but only one CPU component (some machines also support an FPU, but that component * is considered separate from the CPU). * * It's not critical, but it would help catch machine configuration errors; for example, a machine that mistakenly * includes two CPU components may, aside from wasting memory, end up with odd side-effects, like unresponsive * CPU controls. */ Component.add(this); } /** * Component.add(component) * * @param {Component} component */ static add(component) { /* * This just generates a lot of useless noise, handy in the early days, not so much these days.... * * if (DEBUG) Component.log("Component.add(" + component.type + "," + component.id + ")"); */ Component.components.push(component); } /** * Component.addMachine(idMachine) * * @param {string} idMachine */ static addMachine(idMachine) { Component.machines[idMachine] = {}; } /** * Component.addMachineResource(idMachine, sName, data) * * @param {string} idMachine * @param {string|null} sName (name of the resource) * @param {*} data */ static addMachineResource(idMachine, sName, data) { /* * I used to assert(Component.machines[idMachine]), but when we're running as a Node app, embed.js is not used, * so addMachine() is never called, so resources do not need to be recorded. */ if (Component.machines[idMachine] && sName) { Component.machines[idMachine][sName] = data; } } /** * Component.getMachineResources(idMachine) * * @param {string} idMachine * @return {Object|undefined} */ static getMachineResources(idMachine) { return Component.machines[idMachine]; } /** * Component.getTime() * * @return {number} the current time, in milliseconds */ static getTime() { return Date.now() || +new Date(); } /** * Component.log(s, type) * * For diagnostic output only. * * @param {string} [s] is the message text * @param {string} [type] is the message type */ static log(s, type) { if (!COMPILED) { if (s) { var sElapsed = "", sMsg = (type? (type + ": ") : "") + s; if (typeof Usr != "undefined") { if (Component.msStart === undefined) { Component.msStart = Component.getTime(); } sElapsed = (Component.getTime() - Component.msStart) + "ms: "; } if (window && window.console) console.log(sElapsed + sMsg.replace(/\n/g, " ")); } } } /** * Component.assert(f, s) * * Verifies conditions that must be true (for DEBUG builds only). * * The Closure Compiler should automatically remove all references to Component.assert() in non-DEBUG builds. * TODO: Add a task to the build process that "asserts" there are no instances of "assertion failure" in RELEASE builds. * * @param {boolean} f is the expression we are asserting to be true * @param {string} [s] is description of the assertion on failure */ static assert(f, s) { if (DEBUG) { if (!f) { if (!s) s = "assertion failure"; Component.log(s); throw new Error(s); } } } /** * Component.println(s, type, id) * * For non-diagnostic messages, which components may override to control the destination/appearance of their output. * * Components that inherit from this class should use the instance method, this.println(), rather than Component.println(), * because if a Control Panel is loaded, it will override only the instance method, not the class method (overriding the class * method would improperly affect any other machines loaded on the same page). * * @param {string} [s] is the message text * @param {string} [type] is the message type * @param {string} [id] is the caller's ID, if any */ static println(s, type, id) { if (!COMPILED) { Component.log((id? (id + ": ") : "") + (s? ("\"" + s + "\"") : ""), type); } } /** * Component.notice(s, fPrintOnly, id) * * notice() is like println() but implies a need for user notification, so we alert() as well. * * @param {string} s is the message text * @param {boolean} [fPrintOnly] * @param {string} [id] is the caller's ID, if any */ static notice(s, fPrintOnly, id) { if (!COMPILED) { Component.println(s, "notice", id); } if (!fPrintOnly) Component.alertUser((id? (id + ": ") : "") + s); } /** * Component.warning(s) * * @param {string} s describes the warning */ static warning(s) { if (!COMPILED) { Component.println(s, "warning"); } Component.alertUser(s); } /** * Component.error(s) * * @param {string} s describes the error; an alert() is displayed as well */ static error(s) { if (!COMPILED) { Component.println(s, "error"); } Component.alertUser(s); } /** * Component.alertUser(sMessage) * * @param {string} sMessage */ static alertUser(sMessage) { if (window) { window.alert(sMessage); } else { Component.log(sMessage); } }; /** * Component.confirmUser(sPrompt) * * @param {string} sPrompt * @returns {boolean} true if the user clicked OK, false if Cancel/Close */ static confirmUser(sPrompt) { var fResponse = false; if (window) { fResponse = window.confirm(sPrompt); } return fResponse; } /** * Component.promptUser() * * @param {string} sPrompt * @param {string} [sDefault] * @returns {string|null} */ static promptUser(sPrompt, sDefault) { var sResponse = null; if (window) { sResponse = window.prompt(sPrompt, sDefault === undefined? "" : sDefault); } return sResponse; } /** * Component.getComponents(idRelated) * * We could store components as properties, using the component's ID, and change * this linear lookup into a property lookup, but some components may have no ID. * * @param {string} [idRelated] of related component * @return {Array} of components */ static getComponents(idRelated) { var i; var aComponents = []; /* * getComponentByID(id, idRelated) * * If idRelated is provided, we check it for a machine prefix, and use any * existing prefix to constrain matches to IDs with the same prefix, in order to * avoid matching components belonging to other machines. */ if (idRelated) { if ((i = idRelated.indexOf('.')) > 0) idRelated = idRelated.substr(0, i + 1); else idRelated = ""; } for (i = 0; i < Component.components.length; i++) { var component = Component.components[i]; if (!idRelated || !component.id.indexOf(idRelated)) { aComponents.push(component); } } return aComponents; } /** * Component.getComponentByID(id, idRelated) * * We could store components as properties, using the component's ID, and change * this linear lookup into a property lookup, but some components may have no ID. * * @param {string} id of the desired component * @param {string} [idRelated] of related component * @return {Component|null} */ static getComponentByID(id, idRelated) { if (id !== undefined) { var i; /* * If idRelated is provided, we check it for a machine prefix, and use any * existing prefix to constrain matches to IDs with the same prefix, in order to * avoid matching components belonging to other machines. */ if (idRelated && (i = idRelated.indexOf('.')) > 0) { id = idRelated.substr(0, i + 1) + id; } for (i = 0; i < Component.components.length; i++) { if (Component.components[i].id === id) { return Component.components[i]; } } if (Component.components.length) { Component.log("Component ID '" + id + "' not found", "warning"); } } return null; } /** * Component.getComponentByType(sType, idRelated, componentPrev) * * @param {string} sType of the desired component * @param {string} [idRelated] of related component * @param {Component|null} [componentPrev] of previously returned component, if any * @return {Component|null} */ static getComponentByType(sType, idRelated, componentPrev) { if (sType !== undefined) { var i; /* * If idRelated is provided, we check it for a machine prefix, and use any * existing prefix to constrain matches to IDs with the same prefix, in order to * avoid matching components belonging to other machines. */ if (idRelated) { if ((i = idRelated.indexOf('.')) > 0) { idRelated = idRelated.substr(0, i + 1); } else { idRelated = ""; } } for (i = 0; i < Component.components.length; i++) { if (componentPrev) { if (componentPrev == Component.components[i]) componentPrev = null; continue; } if (sType == Component.components[i].type && (!idRelated || !Component.components[i].id.indexOf(idRelated))) { return Component.components[i]; } } Component.log("Component type '" + sType + "' not found", "warning"); } return null; } /** * Component.getComponentParms(element) * * @param {Object} element from the DOM */ static getComponentParms(element) { var parms = null; var sParms = element.getAttribute("data-value"); if (sParms) { try { parms = eval("(" + sParms + ")"); // jshint ignore:line /* * We can no longer invoke removeAttribute() because some components (eg, Panel) need * to run their initXXX() code more than once, to avoid initialization-order dependencies. * * if (!DEBUG) { * element.removeAttribute("data-value"); * } */ } catch(e) { Component.error(e.message + " (" + sParms + ")"); } } return parms; } /** * Component.bindExternalControl(component, sControl, sBinding, sType) * * @param {Component} component * @param {string} sControl * @param {string} sBinding * @param {string} [sType] is the external component type */ static bindExternalControl(component, sControl, sBinding, sType) { if (sControl) { if (sType === undefined) sType = "Panel"; var target = Component.getComponentByType(sType, component.id); if (target) { var eBinding = target.bindings[sControl]; if (eBinding) { component.setBinding(null, sBinding, eBinding); } } } } /** * Component.bindComponentControls(component, element, sAppClass) * * @param {Component} component * @param {Object} element from the DOM * @param {string} sAppClass */ static bindComponentControls(component, element, sAppClass) { var aeControls = Component.getElementsByClass(element.parentNode, sAppClass + "-control"); for (var iControl = 0; iControl < aeControls.length; iControl++) { var aeChildNodes = aeControls[iControl].childNodes; for (var iNode = 0; iNode < aeChildNodes.length; iNode++) { var control = aeChildNodes[iNode]; if (control.nodeType !== 1 /* document.ELEMENT_NODE */) { continue; } var sClass = control.getAttribute("class"); if (!sClass) continue; var aClasses = sClass.split(" "); for (var iClass = 0; iClass < aClasses.length; iClass++) { var parms; sClass = aClasses[iClass]; switch (sClass) { case sAppClass + "-binding": parms = Component.getComponentParms(control); if (parms && parms['binding']) { component.setBinding(parms['type'], parms['binding'], control, parms['value']); } else if (!parms || parms['type'] != "description") { Component.log("Component '" + component.toString() + "' missing binding" + (parms? " for " + parms['type'] : ""), "warning"); } iClass = aClasses.length; break; default: // if (DEBUG) Component.log("Component.bindComponentControls(" + component.toString() + "): unrecognized control class \"" + sClass + "\"", "warning"); break; } } } } } /** * Component.getElementsByClass(element, sClass, sObjClass) * * This is a cross-browser helper function, since not all browser's support getElementsByClassName() * * TODO: This should probably be moved into weblib.js at some point, along with the control binding functions above, * to keep all the browser-related code together. * * @param {Object} element from the DOM * @param {string} sClass * @param {string} [sObjClass] * @return {Array|NodeList} */ static getElementsByClass(element, sClass, sObjClass) { if (sObjClass) sClass += '-' + sObjClass + "-object"; /* * Use the browser's built-in getElementsByClassName() if it appears to be available * (for example, it's not available in IE8, but it should be available in IE9 and up) */ if (element.getElementsByClassName) { return element.getElementsByClassName(sClass); } var i, j, ae = []; var aeAll = element.getElementsByTagName("*"); var re = new RegExp('(^| )' + sClass + '( |$)'); for (i = 0, j = aeAll.length; i < j; i++) { if (re.test(aeAll[i].className)) { ae.push(aeAll[i]); } } if (!ae.length) { Component.log('No elements of class "' + sClass + '" found'); } return ae; } /** * Component.getScriptCommands(sScript) * * This is a simple parser that breaks sScript into an array of commands, where each command * is an array of tokens, where tokens are sequences of characters separated by any of: tab, space, * carriage-return (CR), line-feed (LF), semicolon, single-quote, or double-quote; if a quote is * used, all characters up to the next matching quote become part of the token, allowing any of the * other separators to be part of the token. CR, LF and semicolon also serve to terminate a command, * with semicolon being preferred, because it's 1) more visible, and 2) essential when the entire * script is a multi-line string where all CR/LF were replaced by spaces (which is what Jekyll does, * and since we can't change Jekyll, it's what our own MarkDown Front Matter parser does as well; * see convertMD() in markout.js, where the aCommandDefs array is built). * * Backslash sequences like \n, \r, and \\ have already been converted to LF, CR and backslash * characters, since the entire script string is injected into a JavaScript function call, so any * backslash sequence that JavaScript supports is automatically converted: * * \0 \' \" \\ \n \r \v \t \b \f \uXXXX \xXX * ^J ^M ^K ^I ^H ^L * * To support any other non-printable 8-bit character, such as ESC, you should use \xXX, where XX * is the ASCII code in hex. For ESC, that would \x1B. * * @param {string} sScript * @return {Array} */ static getScriptCommands(sScript) { var cch = sScript.length; var aCommands = [], aTokens = [], sToken = "", chQuote = null; for (var i = 0; i < cch; i++) { var ch = sScript[i]; if (ch == '"' || ch == "'") { if (chQuote && ch != chQuote) { sToken += ch; continue; } if (!chQuote) { chQuote = ch; } else { chQuote = null; } if (sToken) { aTokens.push(sToken); sToken = ""; } continue; } if (!chQuote) { if (ch == '\r' || ch == '\n') { ch = ';'; } if (ch == ' ' || ch == '\t' || ch == ';') { if (sToken) { aTokens.push(sToken); sToken = ""; } if (ch == ';' && aTokens.length) { aCommands.push(aTokens); aTokens = []; } continue; } } sToken += ch; } if (sToken) { aTokens.push(sToken); } if (aTokens.length) { aCommands.push(aTokens); } return aCommands; } /** * Component.processScript(idMachine, sScript) * * @param {string} idMachine * @param {string} sScript * @return {boolean} */ static processScript(idMachine, sScript) { var fSuccess = false; idMachine += ".machine"; if (typeof sScript == "string" && !Component.commands[idMachine]) { fSuccess = true; Component.commands[idMachine] = Component.getScriptCommands(sScript); if (!Component.processCommands(idMachine)) { fSuccess = false; } } return fSuccess; } /** * Component.processCommands(idMachine) * * @param {string} idMachine * @return {boolean} */ static processCommands(idMachine) { var fSuccess = true; var aCommands = Component.commands[idMachine]; // var dbg = Component.getComponentByType("Debugger", idMachine); while (aCommands && aCommands.length) { var aTokens = aCommands.splice(0, 1)[0]; var sCommand = aTokens[0]; /* * It's possible to route this output to the Debugger window with dbg.println() * instead, but it's a bit too confusing mingling script output in a window that * already mingles Debugger and machine output. */ Component.println('script: ' + aTokens.join(' ')); var fnCallReady = null; if (Component.asyncCommands.indexOf(sCommand) >= 0) { fnCallReady = function processNextCommand() { return function() { Component.processCommands(idMachine); } }(); } var fnCommand = Component.globalCommands[sCommand]; if (fnCommand) { if (!fnCallReady) { fSuccess = fnCommand(aTokens[1], aTokens[2], aTokens[3]); } else { if (!fnCommand(fnCallReady, aTokens[1], aTokens[2], aTokens[3])) break; } } else { fSuccess = false; var component = Component.getComponentByType(aTokens[1], idMachine); if (component) { fnCommand = Component.componentCommands[sCommand]; if (fnCommand) { fSuccess = fnCommand(component, aTokens[2], aTokens[3]); } else { var exports = component['exports']; if (exports) { fnCommand = exports[sCommand]; if (fnCommand) { fSuccess = true; if (!fnCallReady) { fSuccess = fnCommand.call(component, aTokens[2], aTokens[3]); } else { if (!fnCommand.call(component, fnCallReady, aTokens[2], aTokens[3])) break; } } } } } } if (!fSuccess) { Component.alertUser("Script error: " + sCommand + (fnCommand? " failed" : " unrecognized")); break; } } if (aCommands && !aCommands.length) { delete Component.commands[idMachine]; } return fSuccess; } /** * Component.scriptAlert(sMessage) * * @param {string} sMessage * @return {boolean} */ static scriptAlert(sMessage) { Component.alertUser(sMessage); return true; } /** * Component.scriptSelect(component, sBinding, sValue) * * @param {Component} component * @param {string} sBinding * @param {string} sValue * @return {boolean} */ static scriptSelect(component, sBinding, sValue) { var fSuccess = false; var aBindings = component['bindings']; var control = aBindings[sBinding]; if (control) { for (var i = 0; i < control.options.length; i++) { if (control.options[i].textContent == sValue) { if (control.selectedIndex != i) { control.selectedIndex = i; } fSuccess = true; break; } } } return fSuccess; } /** * Component.scriptSleep(fnCallback, sDelay) * * @param {function()} fnCallback * @param {string} sDelay (in milliseconds) * @return {boolean} */ static scriptSleep(fnCallback, sDelay) { setTimeout(fnCallback, +sDelay); return false; } /** * toString() * * @this {Component} * @return {string} */ toString() { return (this.name? this.name : (this.id || this.type)); } /** * getMachineNum() * * @this {Component} * @return {number} unique machine number */ getMachineNum() { var nMachine = 1; if (this.idMachine) { var aDigits = this.idMachine.match(/\d+/); if (aDigits !== null) nMachine = parseInt(aDigits[0], 10); } return nMachine; } /** * setBinding(sHTMLType, sBinding, control, sValue) * * Component's setBinding() method is intended to be overridden by subclasses. * * @this {Component} * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "reset") * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) * @param {string} [sValue] optional data value * @return {boolean} true if binding was successful, false if unrecognized binding request */ setBinding(sHTMLType, sBinding, control, sValue) { switch (sBinding) { case "clear": if (!this.bindings[sBinding]) { this.bindings[sBinding] = control; control.onclick = (function(component) { return function clearPanel() { if (component.bindings['print']) { component.bindings['print'].value = ""; } }; }(this)); } return true; case "print": if (!this.bindings[sBinding]) { this.bindings[sBinding] = control; /* * HACK: Save this particular HTML element so that the Debugger can access it, too */ this.controlPrint = control; /* * This was added for Firefox (Safari automatically clears the