Initial commit (a clone of the jsmachines project as of v1.15.3)

This commit is contained in:
jeffpar 2014-09-27 14:52:57 -07:00
commit a5e3e6a59d
714 changed files with 130602 additions and 0 deletions

View file

@ -0,0 +1,28 @@
Shared Sources
===
This folder contains a mix of shared code, with some files used only by Node (server) modules,
some used only by Browser (client) modules, and others used by both.
At the moment, only a few files are completely agnostic; eg: [strlib.js](strlib.js) and [usrlib.js](usrlib.js).
One give-away is that neither contain references to any globals (although references to each other
would be fine).
[netlib.js](netlib.js) is appropriate only for Node modules, because it contains code that relies on Node's
global *Buffer* object, as indicated by:
/* global Buffer: false */
And [weblib.js](weblib.js) is appropriate only for client modules, because it contains code that relies on the
brower's global *window* object, as indicated by:
/* global window: true */
We declare *window* modifiable (true) so that [defines.js](defines.js) can set *global.window* to *false*
when running within Node, allowing any other code to test the existence of *window* with a simple:
if (window) {...}
instead of:
if (typeof window !== 'undefined') {...}

View file

@ -0,0 +1,906 @@
/**
* @fileoverview The Component class used by C1Pjs and PCjs.
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @version 1.0
* Created 2012-May-14
*
* Copyright © 2012-2014 Jeff Parsons <Jeff@pcjs.org>
*
* This file is part of the JavaScript Machines Project (aka JSMachines) at <http://jsmachines.net/>
* and <http://pcjs.org/>.
*
* JSMachines 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.
*
* JSMachines 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 JSMachines.
* If not, see <http://www.gnu.org/licenses/gpl.html>.
*
* You are required to include the above copyright notice in every source code file of every
* copy or modified version of this work, and to display that copyright notice on every screen
* that loads or runs any version of this software (see Computer.sCopyright).
*
* Some JSMachines 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 the
* JSMachines Project for purposes of the GNU General Public License, and the author does not claim
* any copyright as to their contents.
*/
/*
* All the C1Pjs and PCjs components now use JSDoc types, primarily so that Google's Closure Compiler
* will compile everything with ZERO warnings. 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 use JSLint, but it's excessively strict for my taste, so this is the only file
* I tried massaging for JSLint's sake. I gave up when it complained about my use of "while (true)";
* replacing "true" with an assignment expression didn't make it any happier.
*
* I wasn't thrilled about replacing all "++" and "--" operators with "+= 1" and "-= 1", nor about using
* "(s || '')" instead of "(s? s : '')", because while the former may seem simpler, it is NOT more portable.
* It's not that I'm trying to write "portable JavaScript", but some of this code was ported from C code
* I'd written about 14 years earlier, and portability is good, so I see no reason to rewrite code to make
* it less portable.
*
* UPDATE: I've since switched to JSHint, which seems to have more reasonable defaults.
*/
"use strict";
/* global window: true, DEBUG: true */
if (typeof module !== 'undefined') {
require("./defines");
var usr = require("./usrlib");
var web = require("./weblib");
}
/**
* Component(type, parms, constructor)
*
* @constructor
* @param {string} type
* @param {Object} [parms]
* @param {Object} [constructor]
*
* 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)
*
* Subclasses that use Component.subclass() to extend Component will likely have additional (parms) properties.
*/
function Component(type, parms, constructor)
{
this.type = type;
if (!parms) {
parms = {'id': "", 'name': ""};
}
this.id = parms['id'];
this.name = parms['name'];
this.comment = parms['comment'];
this.parms = parms;
if (this.id === undefined) this.id = "";
var i = this.id.indexOf('.');
if (i > 0) {
this.idMachine = this.id.substr(0, i);
this.idComponent = this.id.substr(i + 1);
} else {
this.idComponent = this.id;
}
/*
* Recording the constructor is really just a debugging aid, because many of our constructors
* have class constants, but they're hard to find when the constructors are buried among all the
* other globals.
*/
this[type] = constructor;
/*
* TODO: Decide how to reintegrate this code into the components that still want it....
*
if (this.initStep) this.initStep(parms);
*/
this.fnReady = null;
this.fReady = false;
this.fBusy = this.fBusyCancel = false;
this.fPowered = false;
this.clearError();
this.bindings = {};
this.dbg = null; // by default, no connection to a Debugger
this.getMachineNum(); // once again, make the function appear used
Component.add(this);
}
/**
* Component.parmsURL
*
* Initialized to the set of URL parameters, if any, for the current web page.
*
* @type {Object|null}
*/
Component.parmsURL = web.getURLParameters();
/**
* Component.inherit(p)
*
* Returns a newly created object that inherits properties from the prototype object p.
* It uses the ECMAScript 5 function Object.create() if it is defined, and otherwise falls back to an older technique.
*
* See: Flanagan, David (2011-04-18). JavaScript: The Definitive Guide: The Definitive Guide (Kindle Locations 9854-9903). OReilly Media - A. Kindle Edition (Example 6-1)
*
* @param {Object} p
*/
Component.inherit = function(p)
{
if (window) { // an alternative to "if (typeof window === 'undefined')" if require("defines") has been invoked
if (!p) throw new TypeError(); // TODO: Why does this barf under Node?
if (Object.create) {
return Object.create(p);
}
var t = typeof p;
if (t !== "object" && t !== "function") throw new TypeError();
}
/**
* @constructor
*/
function F() {}
F.prototype = p;
return new F();
};
/**
* Component.extend(o, p)
*
* Copies the enumerable properties of p to o and returns o.
* If o and p have a property by the same name, o's property is overwritten.
*
* See: Flanagan, David (2011-04-18). JavaScript: The Definitive Guide: The Definitive Guide (Kindle Locations 9854-9903). OReilly Media - A. Kindle Edition (Example 6-2)
*
* @param {Object} o
* @param {Object} p
*/
Component.extend = function(o, p)
{
for (var prop in p) {
o[prop] = p[prop];
}
return o;
};
/**
* Component.subclass(superclass, subclass, methods, statics)
*
* TODO: Determine why every subclass created by this function ends up with a name prefix of "Component.subclass"
* in Chrome's call stack, rather than the (more logical) name of the subclass constructor. Is there a different
* design pattern I should be using that creates subclasses more to Chrome's liking?
*
* See: Flanagan, David (2011-04-18). JavaScript: The Definitive Guide: The Definitive Guide (Kindle Locations 9854-9903). OReilly Media - A. Kindle Edition (Example 9-11)
*
* @param {Object} superclass is the constructor of the superclass
* @param {Object} subclass is the constructor for the new subclass
* @param {Object} [methods] contains all instance methods
* @param {Object} [statics] contains all class properties and methods
*/
Component.subclass = function(superclass, subclass, methods, statics)
{
subclass.prototype = Component.inherit(superclass.prototype);
subclass.prototype.constructor = subclass;
if (methods) {
Component.extend(subclass.prototype, methods);
}
if (statics) {
Component.extend(subclass, statics);
}
return subclass;
};
/*
* Every component created on the current page is recorded in this array (see Component.add()).
*
* This enables any component to locate another component by ID (see Component.getComponentByID())
* or by type (see Component.getComponentByType()).
*/
Component.all = [];
/**
* Component.add(component)
*
* @param {Component} component
*/
Component.add = function(component)
{
/*
* This just generates a lot of useless noise, handy in the early days, not so much these days...
*
* Component.log("Component.add(" + component.type + "," + component.id + ")");
*/
Component.all[Component.all.length] = component;
};
/**
* Component.log(s, type)
*
* For diagnostic output only.
*
* @param {string} [s] is the message text
* @param {string} [type] is the message type
*/
Component.log = function(s, type)
{
if (DEBUG) {
var msElapsed, sMsg = (type? (type + ": ") : "") + (s || "");
if (Component.msStart === undefined) {
Component.msStart = usr.getTime();
}
msElapsed = usr.getTime() - Component.msStart;
console.log(sMsg? (msElapsed + "ms: " + sMsg.replace(/\n/g, " ")) : "");
}
};
/**
* Component.assert(f, s)
*
* Used to verify conditions that must be true (for DEBUG builds only; compiled builds should automatically have all
* references to Component.assert() removed).
*
* @param {boolean} f is the expression we are asserting to be true
* @param {string} [s] is description of the assertion on failure
*/
Component.assert = function(f, s)
{
if (DEBUG) {
if (!f) {
if (!s) {
/*
* TODO: An accompanying source file/line number/function call would be nice, if there was a browser-independent way....
*/
s = "assertion failure";
}
Component.log(s);
throw new Error(s);
}
}
};
/**
* Component.println(s, type, id)
*
* For non-diagnostic output, which some components override in order to make their output visible in their own way.
*
* @param {string} [s] is the message text
* @param {string} [type] is the message type
* @param {string} [id] is the caller's ID, if any
*/
Component.println = function(s, type, id)
{
if (DEBUG) {
Component.log((id? (id + ": ") : "") + (s? ("\"" + s + "\"") : ""), type);
}
};
/**
* Component.notice(s, fPrintOnly, id)
*
* notice() is like println() but implies a need for user notification, which means calling log() isn't good enough,
* so we alert() as well; however, if Component.println() is overridden, Component.notice will be replaced with the same
* override, on the assumption that the override is taking care of all user notifications.
*
* @param {string} s is the message text
* @param {boolean} [fPrintOnly]
* @param {string} [id] is the caller's ID, if any
*/
Component.notice = function(s, fPrintOnly, id)
{
if (DEBUG) {
Component.println(s, "notice", id);
}
if (!fPrintOnly) web.alertUser(s);
};
/**
* Component.warning(s)
*
* @param {string} s describes the warning
*/
Component.warning = function(s)
{
if (DEBUG) {
Component.println(s, "warning");
}
web.alertUser(s);
};
/**
* Component.error(s)
*
* @param {string} s describes the error; an alert() is displayed as well
*/
Component.error = function(s)
{
if (DEBUG) {
Component.println(s, "error");
}
web.alertUser(s);
};
/**
* Component.getComponents(idRelated)
*
* We could store components as properties of an 'all' object, 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
*/
Component.getComponents = function(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 = undefined;
}
for (i = 0; i < Component.all.length; i++) {
var component = Component.all[i];
if (!idRelated || !component.id.indexOf(idRelated)) {
aComponents.push(component);
}
}
return aComponents;
};
/**
* Component.getComponentByID(id, idRelated)
*
* We could store components as properties of an 'all' object, 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}
*/
Component.getComponentByID = function(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.all.length; i++) {
if (Component.all[i].id === id) {
return Component.all[i];
}
}
Component.log('Component.getComponentByID("' + id + '"): no component found', "warning");
}
return null;
};
/**
* Component.getComponentByType(sType, idRelated, componentPrev)
*
* @param {string} sType of the desired component
* @param {string} [idRelated] of related component
* @param {Component} [componentPrev] of previously returned component, if any
* @return {Component|null}
*/
Component.getComponentByType = function(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 = undefined;
}
}
for (i = 0; i < Component.all.length; i++) {
if (componentPrev) {
if (componentPrev == Component.all[i]) componentPrev = null;
continue;
}
if (sType == Component.all[i].type && (!idRelated || !Component.all[i].id.indexOf(idRelated))) {
return Component.all[i];
}
}
Component.log('Component.getComponentByType("' + sType + '"): no component found', "warning");
}
return null;
};
/**
* Component.getComponentParms(element)
*
* @param {Object} element from the DOM
*/
Component.getComponentParms = function(element)
{
var parms = null,
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
*/
Component.bindExternalControl = function(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, null, sBinding, eBinding);
}
}
}
};
/**
* Component.bindComponentControls(component, element, sAppClass)
*
* @param {Component} component
* @param {Object} element from the DOM
* @param {string} sAppClass
*/
Component.bindComponentControls = function(component, element, sAppClass)
{
var iControl, aeControls;
aeControls = Component.getElementsByClass(element.parentNode, sAppClass + "-control");
for (iControl = 0; iControl < aeControls.length; iControl++) {
var iNode, aeChildNodes;
aeChildNodes = aeControls[iControl].childNodes;
for (iNode = 0; iNode < aeChildNodes.length; iNode++) {
var control = aeChildNodes[iNode];
if (control.nodeType !== window.document.ELEMENT_NODE) {
continue;
}
var sClass = control.getAttribute("class");
if (!sClass) continue;
var iClass, aClasses;
aClasses = sClass.split(" ");
for (iClass = 0; iClass < aClasses.length; iClass++) {
var parms;
sClass = aClasses[iClass];
switch (sClass) {
case sAppClass + "-input":
case sAppClass + "-output":
parms = Component.getComponentParms(control);
if (parms && parms['binding']) {
component.setBinding(sClass, parms['type'], parms['binding'], control);
} else {
Component.log('Component.bindComponentControls("' + component.toString() + '"): no binding info for ' + parms['type'], "warning");
}
aClasses = [];
break;
default:
// 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}
*/
Component.getElementsByClass = function(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.prototype = {
constructor: Component,
/**
* toString()
*
* @this {Component}
* @return {string}
*/
toString: function() {
return (this.name? this.name : (this.id || this.type));
},
/**
* getMachineNum()
*
* @this {Component}
* @return {number} unique machine number
*/
getMachineNum: function() {
var nMachine = 1;
if (this.idMachine) {
var aDigits = this.idMachine.match(/\d+/);
if (aDigits !== null)
nMachine = parseInt(aDigits[0], 10);
}
return nMachine;
},
/**
* setBinding(sHTMLClass, sHTMLType, sBinding, control)
*
* Component's setBinding() method is intended to be overridden by subclasses. The only
* exception is the Panel component, which passes two special bindings ("clear" and "print")
* back to us if no one else accepted them, so that we can redirect all println() requests
* to those controls.
*
* @this {Component}
* @param {string|null} sHTMLClass is the class of the HTML control (eg, "input", "output")
* @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)
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding: function(sHTMLClass, sHTMLType, sBinding, control) {
switch (sBinding) {
case "clear":
if (!this.bindings[sBinding]) {
this.bindings[sBinding] = control;
control.onclick = (function(component) {
return function() {
if (component.bindings['print']) {
component.bindings['print'].value = "";
}
};
}(this));
}
return true;
case "print":
if (!this.bindings[sBinding]) {
this.bindings[sBinding] = control;
control.value = ""; // this was added for Firefox (Safari automatically clears the <textarea> on a page reload, but Firefox does not)
Component.println = (function(control) {
return function printControl(s, type) {
s = (type !== undefined? (type + ": ") : "") + (s || "");
if (!DEBUG) { // in non-DEBUG builds, prevent the <textarea> from getting too large, otherwise printing becomes slower and slower
if (control.value.length > 8192) {
control.value = control.value.substr(control.value.length - 4096);
}
}
control.value += s + "\n";
control.scrollTop = control.scrollHeight;
if (DEBUG) console.log(s);
};
}(control));
/*
* Override Component.notice() with a replacement function that eliminates the web.alertUser() call
*/
Component.notice = function(s, fPrintOnly, id) {
Component.println(s, "notice", id);
};
/*
* HACK: Save this particular HTML element so that the Debugger can access it, too
*/
Component.controlPrint = control;
}
return true;
default:
/*
* Now that we're giving the Panel component multiple shots at binding its controls,
* to relax initialization dependencies, we need to chill when unrecognized requests come in.
*
if (sHTMLClass == "input") {
control.onclick = function() {
Component.println("unsupported " + sHTMLType + ": " + sBinding);
};
}
this.log("setBinding(\"" + sHTMLClass + "\",\"" + sHTMLType + "\",\"" + sBinding + "\"): unrecognized binding");
*/
break;
}
return false;
},
/**
* log(s, type)
*
* For diagnostic output only.
*
* WARNING: Even though this function's body is completely wrapped in DEBUG, that won't prevent the Closure Compiler
* from including it, so all calls must still be prefixed with "if (DEBUG) ....". For this reason, the class method,
* Component.log(), is preferred, because the compiler IS smart enough to remove those calls.
*
* @this {Component}
* @param {string} [s] is the message text
* @param {string} [type] is the message type
*/
log: function(s, type) {
if (DEBUG) Component.log(s, type || this.id || this.type);
},
/**
* println(s, type)
*
* For non-diagnostic output, which some components override in order to make their output visible in their own way.
*
* @this {Component}
* @param {string} [s] is the message text
* @param {string} [type] is the message type
* @param {string} [id] is the caller's ID, if any
*/
println: function(s, type, id) {
Component.println(s, type, id || this.id);
},
/**
* status(s)
*
* status() is like println() but it also includes information about the component (ie, the component ID),
* which is why there is no corresponding Component.status() function.
*
* @param {string} s is the message text
*/
status: function(s) {
this.println(this.idComponent + ": " + s);
},
/**
* notice(s, fPrintOnly)
*
* notice() is like println() but implies a need for user notification, which means calling log() isn't good enough, so we alert() as well;
* however, if Component.println() is overridden, Component.notice will be replaced with the same override, on the assumption that the override
* is taking care of user notification.
*
* @this {Component}
* @param {string} s is the message text
* @param {boolean} [fPrintOnly]
* @param {string} [id] is the caller's ID, if any
*/
notice: function(s, fPrintOnly, id) {
Component.notice(s, fPrintOnly, id || this.id);
},
/**
* warning(s)
*
* @this {Component}
* @param {string} s describes the warning
*/
warning: function(s) {
Component.warning(s);
},
/**
* error(s)
*
* @this {Component}
* @param {string} s describes the error; an alert() is displayed as well
*/
error: function(s) {
Component.error(s);
},
/**
* setError(s)
*
* Set a fatal error condition
*
* @this {Component}
* @param {string} s describes a fatal error condition
*/
setError: function(s) {
this.fError = true;
this.notice("Fatal error: " + s);
},
/**
* clearError()
*
* Clear any fatal error condition
*
* @this {Component}
*/
clearError: function() {
this.fError = false;
},
/**
* isError()
*
* Report any fatal error condition
*
* @this {Component}
* @return {boolean} true if a fatal error condition exists, false if not
*/
isError: function() {
if (this.fError) {
this.println(this.toString() + " error");
return true;
}
return false;
},
/**
* isReady(fnReady)
*
* Return the "ready" state of the component; if the component is not ready, it will queue the optional
* notification function, otherwise it will immediately call the notification function, if any, without queuing it.
*
* NOTE: Since only the Computer component actually cares about the "readiness" of other components, the so-called
* "queue" of notification functions supports exactly one function. This keeps things nice and simple.
*
* @this {Component}
* @param {function()} [fnReady]
* @return {boolean} true if the component is in a "ready" state, false if not
*/
isReady: function(fnReady) {
if (fnReady) {
if (this.fReady) {
fnReady();
} else {
if (DEBUG) this.log("NOT ready");
this.fnReady = fnReady;
}
}
return this.fReady;
},
/**
* setReady(fReady)
*
* Set the "ready" state of the component to true, and call any queued notification functions.
*
* @this {Component}
* @param {boolean} [fReady] is assumed to indicate "ready" unless EXPLICITLY set to false
*/
setReady: function(fReady) {
if (!this.fError) {
this.fReady = (fReady !== false);
if (this.fReady) {
if (DEBUG || this.name) this.log("ready");
var fnReady = this.fnReady;
this.fnReady = null;
if (fnReady) fnReady();
}
}
},
/**
* isBusy(fCancel)
*
* Return the "busy" state of the component
*
* @this {Component}
* @param {boolean} [fCancel] is set to true to cancel a "busy" state
* @return {boolean} true if "busy", false if not
*/
isBusy: function(fCancel) {
if (this.fBusy) {
if (fCancel) {
this.fBusyCancel = true;
} else if (fCancel === undefined) {
this.println(this.toString() + " busy");
}
}
return this.fBusy;
},
/**
* setBusy(fBusy)
*
* Update the current busy state; if an fCancel request is pending, it will be honored now.
*
* @this {Component}
* @param {boolean} fBusy
* @return {boolean}
*/
setBusy: function(fBusy) {
if (this.fBusyCancel) {
if (this.fBusy) {
this.fBusy = false;
}
this.fBusyCancel = false;
return false;
}
if (this.fError) {
this.println(this.toString() + " error");
return false;
}
this.fBusy = fBusy;
return this.fBusy;
},
/**
* powerUp(fSave)
*
* @this {Component}
* @param {Object|null} data
* @param {boolean} [fRepower] is true if this is "repower" notification
* @return {boolean} true if successful, false if failure
*/
powerUp: function(data, fRepower) {
this.fPowered = true;
return true;
},
/**
* powerDown(fSave, fShutdown)
*
* @this {Component}
* @param {boolean} fSave
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown: function(fSave, fShutdown) {
if (fShutdown) this.fPowered = false;
return true;
}
};
/*
* TODO: What was this work-around for? I forget....
*/
if (window && !window.document.ELEMENT_NODE) window.document.ELEMENT_NODE = 1;
if (typeof module !== 'undefined') module.exports = Component;

View file

@ -0,0 +1,76 @@
/**
* @fileoverview Compile-time definitions used by C1Pjs and PCjs.
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @version 1.0
* Created 2014-May-08
*
* Copyright © 2012-2014 Jeff Parsons <Jeff@pcjs.org>
*
* This file is part of the JavaScript Machines Project (aka JSMachines) at <http://jsmachines.net/>
* and <http://pcjs.org/>.
*
* JSMachines 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.
*
* JSMachines 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 JSMachines.
* If not, see <http://www.gnu.org/licenses/gpl.html>.
*
* You are required to include the above copyright notice in every source code file of every
* copy or modified version of this work, and to display that copyright notice on every screen
* that loads or runs any version of this software (see Computer.sCopyright).
*
* Some JSMachines 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 the
* JSMachines Project for purposes of the GNU General Public License, and the author does not claim
* any copyright as to their contents.
*/
"use strict";
/**
* @define {string}
*/
var APPNAME = ""; // this @define is overridden by the Closure Compiler with either "PCjs" or "C1Pjs"
/**
* @define {string}
*/
var APPVERSION = "1.0.0"; // this @define is overridden by the Closure Compiler with the version in package.json
/**
* @define {string}
*/
var SITEHOST = "pcjs:8088"; // this @define is overridden by the Closure Compiler with "www.pcjs.org"
/**
* @define {boolean}
*/
var COMPILED = false; // this @define is overridden by the Closure Compiler (to true)
/**
* @define {boolean}
*/
var DEBUG = true; // this @define is overridden by the Closure Compiler (to false) to remove DEBUG-only code
/**
* @define {boolean}
*/
var MAXDEBUG = false; // this @define is overridden by the Closure Compiler (to false) to remove MAXDEBUG-only code
if (typeof module !== 'undefined') {
global.window = false;
global.APPNAME = APPNAME;
global.APPVERSION = APPVERSION;
global.SITEHOST = SITEHOST;
global.COMPILED = COMPILED;
global.DEBUG = DEBUG;
global.MAXDEBUG = MAXDEBUG;
/*
* TODO: When we're "required" by Node, should we return anything via module.exports?
*/
}

View file

@ -0,0 +1,75 @@
/**
* @fileoverview Disk APIs, as defined by httpapi.js and consumed by disk.js
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @version 1.0
* Created 2014-May-08
*
* Copyright © 2012-2014 Jeff Parsons <Jeff@pcjs.org>
*
* This file is part of the JavaScript Machines Project (aka JSMachines) at <http://jsmachines.net/>
* and <http://pcjs.org/>.
*
* JSMachines 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.
*
* JSMachines 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 JSMachines.
* If not, see <http://www.gnu.org/licenses/gpl.html>.
*
* You are required to include the above copyright notice in every source code file of every
* copy or modified version of this work, and to display that copyright notice on every screen
* that loads or runs any version of this software (see Computer.sCopyright).
*
* Some JSMachines 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 the
* JSMachines Project for purposes of the GNU General Public License, and the author does not claim
* any copyright as to their contents.
*/
"use strict";
/*
* Our "DiskIO API" looks like:
*
* http://www.pcjs.org/api/v1/disk?action=open&volume=*10mb.img&mode=demandrw&chs=c:h:s&machine=xxx&user=yyy
*/
var DiskAPI = {
ENDPOINT: "/api/v1/disk",
QUERY: {
ACTION: "action", // value is one of DiskAPI.ACTION.*
VOLUME: "volume", // value is path of a disk image
MODE: "mode", // value is one of DiskAPI.MODE.*
CHS: "chs", // value is cylinders:heads:sectors:bytes
ADDR: "addr", // value is cylinder:head:sector:count
MACHINE: "machine", // value is machine token
USER: "user", // value is user ID
DATA: "data" // value is data to be written
},
ACTION: {
OPEN: "open",
READ: "read",
WRITE: "write",
CLOSE: "close"
},
MODE: {
LOCAL: "local", // this mode implies no API (at best, localStorage backing only)
PRELOAD: "preload", // this mode implies use of the DumpAPI
DEMANDRW: "demandrw",
DEMANDRO: "demandro"
},
FAIL: {
BADACTION: "invalid action",
BADUSER: "invalid user",
BADVOL: "invalid volume",
OPENVOL: "unable to open volume",
CREATEVOL: "unable to create volume",
WRITEVOL: "unable to write volume",
REVOKED: "access revoked"
}
};
if (typeof module !== 'undefined') module.exports = DiskAPI;

View file

@ -0,0 +1,85 @@
/**
* @fileoverview Disk APIs, as defined by diskdump.js and consumed by disk.js
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @version 1.0
* Created 2014-May-08
*
* Copyright © 2012-2014 Jeff Parsons <Jeff@pcjs.org>
*
* This file is part of the JavaScript Machines Project (aka JSMachines) at <http://jsmachines.net/>
* and <http://pcjs.org/>.
*
* JSMachines 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.
*
* JSMachines 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 JSMachines.
* If not, see <http://www.gnu.org/licenses/gpl.html>.
*
* You are required to include the above copyright notice in every source code file of every
* copy or modified version of this work, and to display that copyright notice on every screen
* that loads or runs any version of this software (see Computer.sCopyright).
*
* Some JSMachines 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 the
* JSMachines Project for purposes of the GNU General Public License, and the author does not claim
* any copyright as to their contents.
*/
"use strict";
/*
* Our "DiskDump API", such as it was, used to look like:
*
* http://jsmachines.net/bin/convdisk.php?disk=/disks/pc/dos/ibm/2.00/PCDOS200-DISK1.json&format=img
*
* To make it (a bit) more "REST-like", the above request now looks like:
*
* http://www.pcjs.org/api/v1/dump?disk=/disks/pc/dos/ibm/2.00/PCDOS200-DISK1.json&format=img
*
* Similarly, our "FileDump API" used to look like:
*
* http://jsmachines.net/bin/convrom.php?rom=/devices/pc/bios/5150/1981-04-24.rom&format=json
*
* and that request now looks like:
*
* http://www.pcjs.org/api/v1/dump?file=/devices/pc/bios/5150/1981-04-24.rom&format=json
*
* I don't think it makes sense to avoid "query" parameters, because blending the path of a disk image with the
* the rest of the URL would be (a) confusing, and (b) more work to parse.
*/
var DumpAPI = {
ENDPOINT: "/api/v1/dump",
QUERY: {
DIR: "dir", // value is path of a directory (DiskDump only)
DISK: "disk", // value is path of a disk image (DiskDump only)
FILE: "file", // value is path of a ROM image file (FileDump only)
IMG: "img", // alias for DISK
PATH: "path", // value is path of a one or more files (DiskDump only)
FORMAT: "format", // value is one of FORMAT values below
COMMENTS: "comments", // value is either "true" or "false"
DECIMAL: "decimal", // value is either "true" to force all numbers to decimal, "false" or undefined otherwise
MBHD: "mbhd" // value is hard disk size in Mb (formerly "mbsize") (DiskDump only)
},
FORMAT: {
JSON: "json", // default
DATA: "data", // same as "json", but built without JSON.stringify() (DiskDump only)
HEX: "hex", // deprecated
BYTES: "bytes", // displays data as hex bytes; normally used only when comments are enabled
IMG: "img", // returns the raw disk data (ie, using a Buffer object) (DiskDump only)
ROM: "rom" // returns the raw file data (ie, using a Buffer object) (FileDump only)
}
};
/*
* Because we use an overloaded API endpoint (ie, one that's shared with the FileDump module), we must
* also provide a list of commands which, when combined with the endpoint, define a unique request.
*/
DumpAPI.asDiskCommands = [DumpAPI.QUERY.DIR, DumpAPI.QUERY.DISK, DumpAPI.QUERY.PATH];
DumpAPI.asFileCommands = [DumpAPI.QUERY.FILE];
if (typeof module !== 'undefined') module.exports = DumpAPI;

View file

@ -0,0 +1,372 @@
/**
* @fileoverview C1Pjs and PCjs embedding functionality.
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @version 1.0
* @suppress {missingProperties}
* Created 2012-Aug-28
*
* Copyright © 2012-2014 Jeff Parsons <Jeff@pcjs.org>
*
* This file is part of the JavaScript Machines Project (aka JSMachines) at <http://jsmachines.net/>
* and <http://pcjs.org/>.
*
* JSMachines 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.
*
* JSMachines 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 JSMachines.
* If not, see <http://www.gnu.org/licenses/gpl.html>.
*
* You are required to include the above copyright notice in every source code file of every
* copy or modified version of this work, and to display that copyright notice on every screen
* that loads or runs any version of this software (see Computer.sCopyright).
*
* Some JSMachines 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 the
* JSMachines Project for purposes of the GNU General Public License, and the author does not claim
* any copyright as to their contents.
*/
"use strict";
/* global window: true, XSLTProcessor: false, web: true, Component: true, APPNAME: false, APPVERSION: false, DEBUG: true */
if (typeof module !== 'undefined') {
var Component;
var str = require("./strlib");
var web = require("./weblib");
}
/**
* loadXML(sFile, idMachine, sStateFile, fResolve)
*
* 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.
*
* For example, since I've been unable to get the XSLT document() function to work inside any
* 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(),
* which iterates over the XML until all "refs" have been resolved (including any nested
* references).
*
* Also, XSL files with a <!DOCTYPE [...]> cause MSIE's Microsoft.XMLDOM.loadXML() function
* to choke, so I strip that out prior to parsing as well.
*
* TODO: Figure out why the XSLT document() function works great when the web browser loads an
* XML file (and the associated XSL file) itself, but does not work when loading documents via
* 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
*/
function loadXML(sXMLFile, idMachine, sStateFile, fResolve)
{
var response = web.loadResource(sXMLFile);
if (response[0]) {
throw new Error(response[1]);
}
return parseXML(response[1], sXMLFile, idMachine, sStateFile, fResolve);
}
/**
* parseXML(sXML, idMachine, sStateFile, fResolve)
*
* 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|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
*/
function parseXML(sXML, sXMLFile, idMachine, sStateFile, fResolve)
{
var xmlDoc = null;
if (sXML) {
if (fResolve) {
sXML = resolveRefs(sXML);
}
if (idMachine) {
var sURL = sXMLFile;
if (sURL && sURL.indexOf('/') < 0) sURL = window.location.pathname + sURL;
sXML = sXML.replace(/(<machine[^>]*\sid=)(['"]).*?\2/, "$1$2" + idMachine + "$2" + (sStateFile? " state=$2" + sStateFile + "$2" : "") + (sURL? " url=$2" + sURL + "$2" : ""));
}
/*
* 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 "<").
*/
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 <!DOCTYPE [...]> tag.
*/
if (!fResolve) {
sXML = sXML.replace(/<!DOCTYPE(.|[\r\n])*\]>\s*/g, "");
}
xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM");
xmlDoc.async = false;
xmlDoc.loadXML(sXML);
} else {
xmlDoc = (new window.DOMParser()).parseFromString(sXML, "text/xml");
}
} else {
throw new Error("unrecognized XML: " + (sXML.length > 255? sXML.substr(0, 255) + "..." : sXML));
}
}
return [sXML, xmlDoc];
}
/**
* resolvesRefs(sXML)
*
* 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.
*
* @param {string} sXML
* @returns {string} with all tags with "ref" attributes replaced with the referenced file instead
*/
function resolveRefs(sXML)
{
var matchRef, sError;
var reRef = /<([a-z]+)\s+ref="(.*?)"(.*?)\/>/g;
while ((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);
}
}
/*
* Apparently when a Windows Azure server delivers one of my XML files, it may modify the first line:
*
* <?xml version="1.0" encoding="UTF-8"?>\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
}
return sXML;
}
/**
* 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} idElement
* @param {string} sXMLFile
* @param {string} [sXSLFile]
* @param {string} [sStateFile]
* @return {string} containing the complete XML string data, or an error if the XML could not be parsed
*/
function embedMachine(sName, sVersion, idElement, sXMLFile, sXSLFile, sStateFile)
{
var sXML = "", sError = "", eMachine = null;
try {
eMachine = window.document.getElementById(idElement);
if (eMachine) {
var sAppClass = sName.toLowerCase(); // eg, "pcjs" or "c1pjs"
if (!sXSLFile) {
if (DEBUG && sVersion == "1.0.0") {
sXSLFile = "/my_modules/" + sAppClass + "-client/templates/components.xsl";
} else {
sXSLFile = "/versions/" + sAppClass + "/" + sVersion + "/components.xsl";
}
}
var aXML = (sXMLFile.substr(0, 1) == "<" ? parseXML(sXMLFile, null, idElement, sStateFile, false) : loadXML(sXMLFile, idElement, sStateFile, true));
sXML = aXML[0];
var xml = aXML[1];
if (xml) {
aXML = loadXML(sXSLFile);
var xsl = aXML[1];
if (xsl) {
/*
* The <machine> template in components.xsl now generates a "machine div" that makes
* the div we required the caller of embedMachine() to provide redundant, so instead
* of appending this fragment to the caller's node, we REPLACE the caller's node.
* This works only because because we ALSO inject the caller's "machine div" ID into
* the fragment's ID during parseXML().
*
* eMachine.innerHTML = sFragment;
*
* Also, if the transform function fails, make sure you're using the appropriate
* "components.xsl" and not a "machine.xsl", because the latter will not produce valid
* embeddable HTML (and is the most common cause of failure at this final stage).
*/
if (window.ActiveXObject || "ActiveXObject" in window) { // second test is required for IE11 on Windows 8.1
var sFragment = xml['transformNode'](xsl);
if (sFragment) {
eMachine.outerHTML = sFragment;
} else {
Component.log(sError = "transformNodeToObject failed");
}
}
else if (window.document.implementation && window.document.implementation.createDocument) {
var xsltProcessor = new XSLTProcessor();
xsltProcessor['importStylesheet'](xsl);
var eFragment = xsltProcessor['transformToFragment'](xml, window.document);
if (eFragment) {
eMachine.parentNode.replaceChild(eFragment, eMachine);
} else {
Component.log(sError = "transformToFragment failed");
}
} else {
/*
* Perhaps I should have performed this test at the outset; on the other hand, I'm
* not aware of any browsers don't support one or both of the above XSLT transformation
* methods, so treat this as a bug.
*/
Component.log(sError = "unable to transform XML: unsupported browser");
}
} else {
Component.log(sError = "failed to load XSL file: " + sXSLFile);
}
} else {
Component.log(sError = "failed to load XML file: " + sXMLFile);
}
} else {
Component.log(sError = "failed to find machine element: " + idElement);
}
} catch(e) {
sError = e.message;
}
if (sError && eMachine) {
/*
* Our MarkOut module (in convertMDMachineLinks()) creates machine containers that look like this:
*
* <div id="' + sMachineID + '" class="machine-placeholder"><p>Embedded PC</p><p class="machine-warning"></p></div>
*
* with the "machine-warning" paragraph pre-populated with a warning message that the user will
* see if nothing at all happens. But hopefully, in the normal case (and especially the error case),
* *something* will have happened.
*
* Note that it is the HTMLOut module (in processMachines()) that ultimately decides which scripts to
* include and then generates the embedPC() and/or embedC1P() calls.
*/
var aeError = Component.getElementsByClass(eMachine, "machine-warning");
if (aeError[0]) {
aeError[0].innerHTML = "Error: " + str.escapeHTML(sError);
}
}
return sError || sXML;
}
/**
* embedC1P(idElement, sXMLFile, sXSLFile)
*
* @param {string} idElement
* @param {string} sXMLFile
* @param {string} [sXSLFile]
* @return {string} XML string data or error message
*/
function embedC1P(idElement, sXMLFile, sXSLFile)
{
return embedMachine("C1Pjs", APPVERSION, idElement, sXMLFile, sXSLFile);
}
/**
* embedPC(idElement, sXMLFile, sXSLFile, sStateFile)
*
* @param {string} idElement
* @param {string} sXMLFile
* @param {string} [sXSLFile]
* @param {string} [sStateFile]
* @return {string} XML string data or error message
*/
function embedPC(idElement, sXMLFile, sXSLFile, sStateFile)
{
return embedMachine("PCjs", APPVERSION, idElement, sXMLFile, sXSLFile, sStateFile);
}
/**
* Prevent the Closure Compiler from renaming functions we want to export, by adding them
* as (named) properties of a global object.
*/
if (APPNAME == "PCjs") {
window['embedPC'] = embedPC;
}
if (APPNAME == "C1Pjs") {
window['embedC1P'] = embedC1P;
}
window['enableEvents'] = web.enablePageEvents;
window['sendEvent'] = web.sendPageEvent;

View file

@ -0,0 +1,39 @@
/**
* @fileoverview Externs used by PCjs and C1Pjs (for the Closure Compiler)
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @version 1.0
* Created 2012-Dec-04
*
* Copyright © 2012-2014 Jeff Parsons <Jeff@pcjs.org>
*
* This file is part of the JavaScript Machines Project (aka JSMachines) at <http://jsmachines.net/>
* and <http://pcjs.org/>.
*
* JSMachines 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.
*
* JSMachines 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 JSMachines.
* If not, see <http://www.gnu.org/licenses/gpl.html>.
*
* You are required to include the above copyright notice in every source code file of every
* copy or modified version of this work, and to display that copyright notice on every screen
* that loads or runs any version of this software (see Computer.sCopyright).
*
* Some JSMachines 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 the
* JSMachines Project for purposes of the GNU General Public License, and the author does not claim
* any copyright as to their contents.
*/
"use strict";
/*
* Unless we declare "module", even code like "if (typeof module === 'undefined') ..." is disallowed by the compiler.
*/
var module;
var webkitAudioContext;

View file

@ -0,0 +1,385 @@
/**
* @fileoverview Net-related functions
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a> (@jeffpar)
* @version 1.0
* Created 2014-03-16
*
* Copyright © 2012-2014 Jeff Parsons <Jeff@pcjs.org>
*
* This file is part of the JavaScript Machines Project (aka JSMachines) at <http://jsmachines.net/>
* and <http://pcjs.org/>.
*
* JSMachines 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.
*
* JSMachines 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 JSMachines.
* If not, see <http://www.gnu.org/licenses/gpl.html>.
*
* You are required to include the above copyright notice in every source code file of every
* copy or modified version of this work, and to display that copyright notice on every screen
* that loads or runs any version of this software (see Computer.sCopyright).
*
* Some JSMachines 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 the
* JSMachines Project for purposes of the GNU General Public License, and the author does not claim
* any copyright as to their contents.
*/
"use strict";
/* global Buffer: false */
var net = {};
if (typeof module !== 'undefined') {
var sServerRoot;
var fs = require("fs");
var http = require("http");
var path = require("path");
var url = require("url");
var str = require("./strlib");
}
/*
* The following are (super-secret) commands that can be added to the URL to enable special features.
*
* Our super-secret command processor is affectionately call Gort, and while Gort doesn't understand commands
* like "Klaatu barada nikto", it does understand commands like "debug" and "rebuild"; eg:
*
* http://www.pcjs.org/?gort=debug
*
* hasParm() detects the presence of the specified command, and propagateParms() is a URL filter that ensures
* any commands listed in asPropagate are passed through to all other URLs on the same page; using any of these
* commands also forces the page to be rebuilt and not cached (since we would never want a cached "index.html" to
* contain/expose any of these commands).
*/
net.GORT_COMMAND = "gort";
net.GORT_DEBUG = "debug"; // use this to force uncompiled JavaScript even on a Release server
net.GORT_NODEBUG = "nodebug"; // use this to force uncompiled JavaScript but with DEBUG code disabled
net.GORT_RELEASE = "release"; // use this to force the use of compiled JavaScript even on a Debug server
net.GORT_REBUILD = "rebuild"; // use this to force the "index.html" in the current directory to be rebuilt
net.REVEAL_COMMAND = "reveal";
net.REVEAL_PDFS = "pdfs";
/*
* This is a list of the URL parameters that propagateParms() will propagate from the requester's URL to
* other URLs provided by the requester.
*/
var asPropagate = [net.GORT_COMMAND, "autostart"];
/**
* hasParm(sParm, sValue, req)
*
* @param {string} sParm
* @param {string|null} sValue (pass null to check for the presence of ANY sParm)
* @param {Object} [req] is the web server's (ie, Express) request object, if any
* @return {boolean} true if the request Object contains the specified parameter/value, false if not
*
* TODO: Consider whether sParm === null should check for the presence of ANY parameter in asPropagate.
*/
net.hasParm = function(sParm, sValue, req)
{
return (req && req.query && req.query[sParm] && (!sValue || req.query[sParm] == sValue));
};
/**
* propagateParms(sURL, req)
*
* Propagates any "special" query parameters (as listed in asPropagate) from the given
* request object (req) to the given URL (sURL).
*
* We do not modify an sURL that already contains a '?' OR that begins with a protocol
* (eg, http:, mailto:, etc), in order to keep this function simple, since it's only for
* debugging purposes anyway. I also considered blowing off any URLs with a '#' for the
* same reason, since any hash string must follow any query parameters, but stripping
* and re-appending the hash string is pretty trivial, so we do handle that.
*
* TODO: Make propagateParms() more general-purpose (eg, capable of detecting any URL
* to the same site, and capable of merging any of our "special" query parameters with any
* existing query parameters.
*
* @param {string|null} sURL
* @param {Object} [req] is the web server's (ie, Express) request object, if any
* @return {string} massaged sURL
*/
net.propagateParms = function(sURL, req)
{
if (sURL !== null && sURL.indexOf('?') < 0) {
var i;
var sHash = "";
if ((i = sURL.indexOf('#')) >= 0) {
sHash = sURL.substr(i);
sURL = sURL.substr(0, i);
}
var match = sURL.match(/^([a-z])+:(.*)/);
if (!match && req && req.query) {
for (i = 0; i < asPropagate.length; i++) {
var sQuery = asPropagate[i];
var sValue;
if ((sValue = req.query[sQuery])) {
var sParm = (sURL.indexOf('?') < 0? '?' : '&');
sParm += sQuery + '=';
if (sURL.indexOf(sParm) < 0) sURL += sParm + encodeURIComponent(sValue);
}
}
}
sURL += sHash;
}
return sURL;
};
/**
* encodeURL(sURL, req, fDebug)
*
* Used to encodes any URLs presented on the current page, using this 3-step process:
*
* 1) Replace any backslashes with slashes, in case the URL was derived from a file system path
* 2) Remap links that begin with "static/" to the corresponding URL at "http://static.pcjs.org/"
* 3) Massage the result with net.propagateParms(), so that any special parameters are passed along
* 4) Transform any "htmlspecialchars" into the corresponding entities, to help ensure proper validation
*
* @param {string} sURL
* @param {Object} req is the web server's (ie, Express) request object, if any
* @param {boolean} [fDebug]
* @return {string} encoded URL
*/
net.encodeURL = function(sURL, req, fDebug)
{
if (sURL) {
sURL = sURL.replace(/\\/g, '/');
if (!fDebug) {
if (sURL.match(/^[^:]*static/)) {
sURL = "http://static.pcjs.org" + path.join(req.path, sURL).replace("/static/", "/");
}
}
return net.propagateParms(encodeURI(sURL), req);
}
return sURL;
};
/**
* isRemote(sPath)
*
* @param {string} sPath
* @return {boolean} true if sPath is a (supported) remote path, false if not
*
* TODO: Add support for FTP? HTTPS? Anything else?
*/
net.isRemote = function(sPath)
{
return (sPath.indexOf("http:") === 0);
};
/**
* getStat(sURL, done)
*
* @param {string} sURL
* @param {function(Error,Object)} done
*/
net.getStat = function(sURL, done)
{
var options = url.parse(sURL);
options.method = "HEAD";
options.path = options.pathname; // TODO: Determine the necessity of aliasing this
var req = http.request(options, function(res) {
var err = null;
var stat = null;
// console.log(JSON.stringify(res.headers));
if (res.statusCode == 200) {
/*
* Apparently Node lower-cases response headers (at least incoming headers, despite
* lots of amusing whining by certain people in the Node community), which seems like
* a good thing, because that means I can do two simple key look-ups.
*/
var sLength = res.headers['content-length'];
var sModified = res.headers['last-modified'];
stat = {
size: sLength? parseInt(sLength, 10) : -1,
mtime: sModified? new Date(sModified) : null,
remote: true // an additional property we provide to indicate this is not your normal stats object
};
} else {
err = new Error("unexpected response code: " + res.statusCode);
}
done(err, stat);
});
req.on('error', function(err) {
done(err, null);
});
req.end();
};
/**
* getFile(sURL, sEncoding, done)
*
* @param {string} sURL is the source file
* @param {string|null} sEncoding is the encoding to assume, if any
* @param {function(Error,number,(string|Buffer))} done receives an Error, an HTTP status code, and a Buffer (if any)
*
* TODO: Add support for FTP? HTTPS? Anything else?
*/
net.getFile = function(sURL, sEncoding, done)
{
/*
* Buffer objects are a fixed size, so my choices are: 1) call getStat() first, hope it returns
* the true size, and then preallocate a buffer; or 2) create a new, larger buffer every time a new
* chunk arrives. The latter seems best.
*
* However, if an encoding is given, we'll simply concatenate all the data into a String and return
* that instead. Note that the incoming data is always a Buffer, but concatenation with a String
* performs an implied "toString()" on the Buffer.
*
* WARNING: Even when an encoding is provided, we don't make any attempt to verify that the incoming
* data matches that encoding.
*/
var sFile = "";
var bufFile = null;
http.get(sURL, function(res) {
res.on('data', function(data) {
if (sEncoding) {
sFile += data;
return;
}
if (!bufFile) {
bufFile = data;
return;
}
/*
* We need to grow bufFile. I used to do this myself, using the "copy" method:
*
* buf.copy(targetBuffer, [targetStart], [sourceStart], [sourceEnd])
*
* which defaults to 0 for [targetStart] and [sourceStart], but the docs don't clearly
* define the default value for [sourceEnd]. They say "buffer.length", but there is no
* parameter here named "buffer". Let's hope that in the case of "bufFile.copy(buf)"
* they meant "bufFile.length".
*
* However, it turns out this is moot, because there's a new kid in town: Buffer.concat().
*
* buf = new Buffer(bufFile.length + data.length);
* bufFile.copy(buf);
* data.copy(buf, bufFile.length);
* bufFile = buf;
*/
bufFile = Buffer.concat([bufFile, data], bufFile.length + data.length);
}).on('end', function() {
/*
* TODO: Decide what to do when res.statusCode is actually an error code (eg, 404), because
* in such cases, the file content will likely just be an HTML error page.
*/
if (res.statusCode < 400) {
done(null, res.statusCode, sEncoding? sFile : bufFile);
} else {
done(new Error(sEncoding? sFile : bufFile), res.statusCode, null);
}
}).on('error', function(err) {
done(err, res.statusCode, null);
});
});
};
/**
* downloadFile(sURL, sFile, done)
*
* @param {string} sURL is the source file
* @param {string} sFile is a fully-qualified target file
* @param {function(Error,number)} done is a callback that receives an Error and a HTTP status code
*/
net.downloadFile = function(sURL, sFile, done)
{
var file = fs.createWriteStream(sFile);
/*
* http.get() accepts a "url" string in lieu of an "options" object; it automatically builds
* the latter from the former using url.parse(). This is good, because it relieves me from
* building my own "options" object, and also from wondering why http functions expect "options"
* to contain a "path" property, whereas url.parse() returns a "pathname" property.
*
* Either the documentation isn't quite right for url.parse() or http.request() (the big brother
* of http.get), or one of those "options" properties is aliased to the other, or...?
*/
http.get(sURL, function(res) {
res.on('data', function(data) {
file.write(data);
}).on('end', function() {
file.end();
/*
* TODO: We should try to update the file's modification time to match the 'last-modified'
* response header value, if any.
*
* TODO: Decide what to do when res.statusCode is actually an error code (eg, 404), because
* in such cases, the file content will likely just be an HTML error page.
*/
done(null, res.statusCode);
}).on('error', function(err) {
done(err, res.statusCode);
});
});
};
/**
* loadResource(sURL, fAsync, data, componentNotify, fnNotify, pNotify)
*
* Request the specified resource (sURL), and once the request is complete,
* optionally call the specified method (fnNotify) of the specified component (componentNotify).
*
* TODO: Figure out how we can strongly type the fnNotify parameter, because the Closure Compiler has issues with:
*
* {function(this:Component, string, (string|null), number, (number|string|null|Object|Array|undefined))} [fnNotify]
*
* NOTE: This function is a mirror image of the weblib version, for server-side component testing within Node;
* since it is NOT intended for production use, it may make liberal use of synchronous functions, warning messages, etc.
*
* @param {string} sURL
* @param {boolean} [fAsync] is true for an asynchronous request
* @param {Object|null} [data] for a POST request (default is a GET request)
* @param {Component} [componentNotify]
* @param {function(...)} [fnNotify]
* @param {number|string|null|Object|Array} [pNotify] optional fnNotify parameter
* @return {Array} containing errorCode and responseText (empty array if async request)
*/
net.loadResource = function(sURL, fAsync, data, componentNotify, fnNotify, pNotify) {
var nErrorCode = -1;
var sResponse = null;
if (net.isRemote(sURL)) {
console.log('net.loadResource("' + sURL + '"): unimplemented');
} else {
if (!sServerRoot) {
sServerRoot = path.join(path.dirname(fs.realpathSync(__filename)), "../../../");
}
var sFile = path.join(sServerRoot, sURL);
if (fAsync) {
fs.readFile(sFile, {encoding: "utf8"}, function(err, s) {
/*
* TODO: If err is set, is there an error code we should return (instead of -1)?
*/
if (!err) {
sResponse = s;
nErrorCode = 0;
}
if (componentNotify && fnNotify) fnNotify.call(componentNotify, str.getBaseName(sURL), sResponse, nErrorCode, pNotify);
});
return [];
} else {
try {
sResponse = fs.readFileSync(sFile, {encoding: "utf8"});
nErrorCode = 0;
} catch(err) {
/*
* TODO: If err is set, is there an error code we should return (instead of -1)?
*/
console.log(err.message);
}
if (componentNotify && fnNotify) fnNotify.call(componentNotify, str.getBaseName(sURL), sResponse, nErrorCode, pNotify);
}
}
return [nErrorCode, sResponse];
};
if (typeof module !== 'undefined') module.exports = net;

View file

@ -0,0 +1,45 @@
/**
* @fileoverview Compile-time definitions for non-DEBUG configurations.
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @version 1.0
* Created 2014-Aug-22
*
* Copyright © 2012-2014 Jeff Parsons <Jeff@pcjs.org>
*
* This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines)
* at <http://jsmachines.net/> and <http://pcjs.org/>.
*
* 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 <http://www.gnu.org/licenses/gpl.html>.
*
* You are required to include the above copyright notice in every source code file of every
* copy or modified version of this work, and to display that copyright notice on every screen
* that loads or runs any version of this software (see Computer.sCopyright).
*
* 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 the
* PCjs program for purposes of the GNU General Public License, and the author does not claim
* any copyright as to their contents.
*/
"use strict";
/* global DEBUG: true */
/*
* In the compiled case, we rely on the Closure Compiler to override DEBUG, setting it to false,
* so that all DEBUG-only code will be removed by the compiler.
*
* However, when we're in "development mode" and want to run uncompiled code without any DEBUG-only
* code, we must arrange for this additional file, nodebug.js, to be loaded as early as possible,
* which will then set DEBUG to false at runtime.
*/
DEBUG = false;

View file

@ -0,0 +1,90 @@
/**
* @fileoverview Process-related helper functions
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a> (@jeffpar)
* @version 1.0
* Created 2014-05-07
*
* Copyright © 2012-2014 Jeff Parsons <Jeff@pcjs.org>
*
* This file is part of the JavaScript Machines Project (aka JSMachines) at <http://jsmachines.net/>
* and <http://pcjs.org/>.
*
* JSMachines 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.
*
* JSMachines 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 JSMachines.
* If not, see <http://www.gnu.org/licenses/gpl.html>.
*
* You are required to include the above copyright notice in every source code file of every
* copy or modified version of this work, and to display that copyright notice on every screen
* that loads or runs any version of this software (see Computer.sCopyright).
*
* Some JSMachines 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 the
* JSMachines Project for purposes of the GNU General Public License, and the author does not claim
* any copyright as to their contents.
*/
"use strict";
var proc = {};
/**
* getArgs()
*
* Processes command-line arguments. Arguments may be introduced by either
* a double-hyphen (--) or a long dash (), and argument values, if any, must be
* separated by an "=" without any intervening whitespace. Arguments without
* an explicit value default to true, and any argument appearing more than once
* is automatically converted to an Array.
*
* Single-hyphen (-) arguments are allowed as well; they are treated as a series
* of single-character arguments, each set to true, and any of these arguments
* appearing more than once is discarded.
*
* @return {{argc:number, argv:{}}}
*/
proc.getArgs = function() {
var argc = 0;
var argv = {};
for (var i = 2; i < process.argv.length; i++) {
var j, sSep;
var sArg = process.argv[i];
if (!sArg.indexOf(sSep = "--") || !sArg.indexOf(sSep = "—")) {
sArg = sArg.substr(sSep.length);
var sValue = true;
j = sArg.indexOf("=");
if (j > 0) {
sValue = sArg.substr(j+1);
sArg = sArg.substr(0, j);
sValue = (sValue == "true")? true : ((sValue == "false")? false : sValue);
}
if (argv[sArg] === undefined) {
argc++;
argv[sArg] = sValue;
} else {
// console.log("too many '" + sArg + "' arguments");
if (typeof argv[sArg] == "string") {
argv[sArg] = [argv[sArg]];
}
argv[sArg].push(sValue);
}
} else if (!sArg.indexOf("-")) {
for (j = 1; j < sArg.length; j++) {
var ch = sArg.charAt(j);
if (argv[ch] === undefined) {
argv[ch] = true;
argc++;
}
}
}
}
return {argc: argc, argv: argv};
};
if (typeof module !== 'undefined') module.exports = proc;

View file

@ -0,0 +1,53 @@
/**
* @fileoverview Report API, as defined by httpapi.js
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @version 1.0
* Created 2014-May-13
*
* Copyright © 2012-2014 Jeff Parsons <Jeff@pcjs.org>
*
* This file is part of the JavaScript Machines Project (aka JSMachines) at <http://jsmachines.net/>
* and <http://pcjs.org/>.
*
* JSMachines 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.
*
* JSMachines 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 JSMachines.
* If not, see <http://www.gnu.org/licenses/gpl.html>.
*
* You are required to include the above copyright notice in every source code file of every
* copy or modified version of this work, and to display that copyright notice on every screen
* that loads or runs any version of this software (see Computer.sCopyright).
*
* Some JSMachines 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 the
* JSMachines Project for purposes of the GNU General Public License, and the author does not claim
* any copyright as to their contents.
*/
"use strict";
var ReportAPI = {
ENDPOINT: "/api/v1/report",
QUERY: {
APP: "app",
VER: "ver",
URL: "url",
USER: "user",
TYPE: "type",
DATA: "data"
},
TYPE: {
BUG: "bug"
},
RES: {
OK: "Thank you"
}
};
if (typeof module !== 'undefined') module.exports = ReportAPI;

View file

@ -0,0 +1,43 @@
/**
* @fileoverview Experimental code included by HTMLOut() only if "--sockets"
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @version 1.0
* Created 2014-Apr-29
*
* Copyright © 2012-2014 Jeff Parsons <Jeff@pcjs.org>
*
* This file is part of the JavaScript Machines Project (aka JSMachines) at <http://jsmachines.net/>
* and <http://pcjs.org/>.
*
* JSMachines 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.
*
* JSMachines 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 JSMachines.
* If not, see <http://www.gnu.org/licenses/gpl.html>.
*
* You are required to include the above copyright notice in every source code file of every
* copy or modified version of this work, and to display that copyright notice on every screen
* that loads or runs any version of this software (see Computer.sCopyright).
*
* Some JSMachines 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 the
* JSMachines Project for purposes of the GNU General Public License, and the author does not claim
* any copyright as to their contents.
*/
"use strict";
/* global io: false */
// connect to the socket server
var socket = io.connect();
// if we get an "info" emit from the socket server then console.log the data we receive
socket.on('info', function (data) {
console.log(data);
});

View file

@ -0,0 +1,323 @@
/**
* @fileoverview String-related helper functions
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a> (@jeffpar)
* @version 1.0
* Created 2014-03-09
*
* Copyright © 2012-2014 Jeff Parsons <Jeff@pcjs.org>
*
* This file is part of the JavaScript Machines Project (aka JSMachines) at <http://jsmachines.net/>
* and <http://pcjs.org/>.
*
* JSMachines 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.
*
* JSMachines 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 JSMachines.
* If not, see <http://www.gnu.org/licenses/gpl.html>.
*
* You are required to include the above copyright notice in every source code file of every
* copy or modified version of this work, and to display that copyright notice on every screen
* that loads or runs any version of this software (see Computer.sCopyright).
*
* Some JSMachines 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 the
* JSMachines Project for purposes of the GNU General Public License, and the author does not claim
* any copyright as to their contents.
*/
"use strict";
var str = {};
/**
* isValidInt(s, base)
*
* Since the built-in parseInt() function has the annoying feature of returning a partial value
* when it encounters an invalid character (eg, parseInt("foo", 16) returns 0xf), use this function
* to validate the entire string first.
*
* @param {string} s is the string representation of some number
* @param {number} [base] is the radix of the number represented above (only 10 and 16 are supported)
* @return {boolean} true if valid (or we're not sure because the base isn't recognized), false if invalid
*/
str.isValidInt = function(s, base)
{
if (!base || base == 10) return s.match(/^[0-9]+$/) !== null;
if (base == 16) return s.match(/^[0-9a-f]+$/i) !== null;
return true;
};
/**
* parseInt(s, base)
*
* This is a wrapper around the built-in parseInt() function, which recognizes certain prefixes (eg,
* '$' or "0x" for hex) and suffixes (eg, 'h' for hex or '.' for decimal), and then calls isValidInt()
* to ensure we don't get partial values (see isValidInt() for details).
*
* @param {string} s is the string representation of some number
* @param {number} [base] is the radix to assume (default is 16)
* @return {number|undefined} corresponding value, or undefined if invalid
*/
str.parseInt = function(s, base)
{
var value;
if (s) {
if (!base) base = 16;
if (s.charAt(0) == '$') {
base = 16;
s = s.substr(1);
} else if (s.substr(0, 2) == "0x") {
base = 16;
s = s.substr(2);
} else {
var chSuffix = s.charAt(s.length-1).toLowerCase();
if (chSuffix == 'h') {
base = 16;
chSuffix = null;
}
else if (chSuffix == '.') {
base = 10;
chSuffix = null;
}
if (chSuffix === null) s = s.substr(0, s.length-1);
}
var v;
if (str.isValidInt(s, base) && !isNaN(v = parseInt(s, base))) {
value = v;
}
}
return value;
};
/**
* toHex(n, cch)
*
* Converts an integer to hex, with the specified number of digits (up to the default of 8).
*
* You might be tempted to use the built-in n.toString(16) instead, but it doesn't zero-pad and it
* doesn't properly convert negative values; for example, if n is -2147483647, then n.toString(16)
* will return "-7fffffff" instead of "80000001". Moreover, if n is undefined, n.toString() will throw
* an exception, whereas toHex() will simply return '?' characters.
*
* NOTE: The following work-around (adapted from code found on StackOverflow) would be another solution,
* taking care of negative values, zero-padding, and upper-casing, but not undefined/NaN values:
*
* s = (n < 0? (0xffffffff + n + 1) : n).toString(16);
* s = "00000000".substr(0, 8 - s.length) + s;
* s = s.substr(0, cch).toUpperCase();
*
* @param {number|undefined} n is a 32-bit value
* @param {number} [cch] is the desired number of hex digits (8 is both the default and the maximum)
* @return {string} the hex representation of n
*/
str.toHex = function(n, cch)
{
var s = "";
if (cch === undefined) {
cch = 8;
} else {
if (cch > 8) cch = 8;
}
if (isNaN(n)) { // detects BOTH NaN and undefined
while (cch-- > 0) s = '?' + s;
} else {
while (cch-- > 0) {
var d = n & 0xf;
d += (d >= 0 && d <= 9? 0x30 : 0x41 - 10);
s = String.fromCharCode(d) + s;
n >>= 4;
}
}
return s;
};
/**
* toHexByte(b)
*
* Alias for toHex(b, 2)
*
* @param {number|undefined} b is a byte value
* @return {string} the hex representation of b
*/
str.toHexByte = function(b)
{
return str.toHex(b, 2);
};
/**
* toHexWord(w)
*
* Alias for toHex(w, 4)
*
* @param {number|undefined} w is a word (16-bit) value
* @return {string} the hex representation of w
*/
str.toHexWord = function(w)
{
return str.toHex(w, 4);
};
/**
* toHexAddr(off, sel)
*
* @param {number} off
* @param {number} [sel]
* @return {string} the hex representation of sel:off
*/
str.toHexAddr = function(off, sel)
{
if (sel !== undefined) {
return str.toHexWord(sel) + ":" + str.toHexWord(off);
}
return str.toHex(off);
};
/**
* getBaseName(sFileName, fStripExt)
*
* This is a poor-man's version of Node's path.basename(), which Node-only components should use instead.
*
* Note that fStripExt can be used to strip ANY extension, whereas path.basename() will strip the extension only
* if it matches the second parameter (eg, path.basename("/foo/bar/baz/asdf/quux.html", ".html") returns "quux").
*
* @param {string} sFileName
* @param {boolean} [fStripExt]
* @return {string}
*/
str.getBaseName = function(sFileName, fStripExt)
{
var sBaseName = sFileName;
var i = sFileName.lastIndexOf("/");
if (i >= 0) {
sBaseName = sFileName.substr(i + 1);
}
if (fStripExt) {
i = sBaseName.lastIndexOf(".");
if (i > 0) {
sBaseName = sBaseName.substring(0, i);
}
}
return sBaseName;
};
/**
* getExtension(sFileName)
*
* This is a poor-man's version of Node's path.extname(), which Node-only components should use instead.
*
* Note that we EXCLUDE the period from the returned extension, whereas path.extname() includes it.
*
* @param {string} sFileName
* @return {string} the filename's extension (in lower-case and EXCLUDING the "."), or an empty string
*/
str.getExtension = function(sFileName)
{
var sExtension = "";
var i = sFileName.lastIndexOf(".");
if (i >= 0) {
sExtension = sFileName.substr(i + 1).toLowerCase();
}
return sExtension;
};
/**
* endsWith(s, sSuffix)
*
* @param {string} s
* @param {string} sSuffix
* @return {boolean} true if s ends with sSuffix, false if not
*/
str.endsWith = function(s, sSuffix)
{
return s.indexOf(sSuffix, s.length - sSuffix.length) !== -1;
};
str.aHTMLEscapeMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
/**
* escapeHTML(sHTML)
*
* @param {string} sHTML
* @return {string} with HTML entities "escaped", similar to PHP's htmlspecialchars()
*/
str.escapeHTML = function(sHTML)
{
return sHTML.replace(/[&<>"']/g, function(m) {
return str.aHTMLEscapeMap[m];
});
};
/**
* replaceAll(sFind, sReplace, s)
*
* @param {string} sFind
* @param {string} sReplace
* @param {string} s
* @return {string}
*/
str.replaceAll = function(sFind, sReplace, s)
{
var a = {};
a[sFind] = sReplace;
return str.replaceArray(a, s);
};
/**
* replaceArray(a, s)
*
* @param {Object} a
* @param {string} s
* @return {string}
*/
str.replaceArray = function(a, s)
{
var sMatch = "";
for (var k in a) {
k = k.replace(/([\\\[\]\*\{}\(\)\.\+\?])/g, "\\$1");
sMatch += (sMatch? '|' : '') + k;
}
return s.replace(new RegExp('(' + sMatch + ')', "g"), function(m) {
return a[m];
});
};
/**
* pad(s, cch)
*
* Note that the maximum amount of padding currently supported is 40 spaces.
*
* @param {string} s is a string
* @param {number} cch is desired length
* @returns {string} the original string (s) with spaces padding it to the specified length
*/
str.pad = function(s, cch)
{
return s + " ".substr(0, cch - s.length);
};
/**
* trim(s)
*
* @param {string} s
* @returns {string}
*/
str.trim = function(s)
{
if (String.prototype.trim) {
return s.trim();
}
return s.replace(/^\s+|\s+$/g, "");
};
if (typeof module !== 'undefined') module.exports = str;

View file

@ -0,0 +1,73 @@
/**
* @fileoverview User API, as defined by httpapi.js
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @version 1.0
* Created 2014-May-13
*
* Copyright © 2012-2014 Jeff Parsons <Jeff@pcjs.org>
*
* This file is part of the JavaScript Machines Project (aka JSMachines) at <http://jsmachines.net/>
* and <http://pcjs.org/>.
*
* JSMachines 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.
*
* JSMachines 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 JSMachines.
* If not, see <http://www.gnu.org/licenses/gpl.html>.
*
* You are required to include the above copyright notice in every source code file of every
* copy or modified version of this work, and to display that copyright notice on every screen
* that loads or runs any version of this software (see Computer.sCopyright).
*
* Some JSMachines 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 the
* JSMachines Project for purposes of the GNU General Public License, and the author does not claim
* any copyright as to their contents.
*/
"use strict";
/*
* Examples of User API requests:
*
* web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.VERIFY + '&' + UserAPI.QUERY.USER + '=' + sUser;
*/
var UserAPI = {
ENDPOINT: "/api/v1/user",
QUERY: {
REQ: "req", // specifies a request
USER: "user", // specifies a user ID
STATE: "state", // specifies a state ID
DATA: "data" // specifies state data
},
REQ: {
CREATE: "create", // creates a user ID
VERIFY: "verify", // requests verification of a user ID
STORE: "store", // stores a machine state on the server
LOAD: "load" // loads a machine state from the server
},
RES: {
CODE: "code",
DATA: "data"
},
CODE: {
OK: "ok",
FAIL: "error"
},
FAIL: {
DUPLICATE: "user already exists",
VERIFY: "unable to verify user",
BADSTATE: "invalid state parameter",
NOSTATE: "no machine state",
BADLOAD: "unable to load machine state",
BADSTORE: "unable to save machine state"
}
};
if (typeof module !== 'undefined') module.exports = UserAPI;

View file

@ -0,0 +1,212 @@
/**
* @fileoverview Assorted helper functions
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a> (@jeffpar)
* @version 1.0
* Created 2014-03-09
*
* Copyright © 2012-2014 Jeff Parsons <Jeff@pcjs.org>
*
* This file is part of the JavaScript Machines Project (aka JSMachines) at <http://jsmachines.net/>
* and <http://pcjs.org/>.
*
* JSMachines 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.
*
* JSMachines 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 JSMachines.
* If not, see <http://www.gnu.org/licenses/gpl.html>.
*
* You are required to include the above copyright notice in every source code file of every
* copy or modified version of this work, and to display that copyright notice on every screen
* that loads or runs any version of this software (see Computer.sCopyright).
*
* Some JSMachines 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 the
* JSMachines Project for purposes of the GNU General Public License, and the author does not claim
* any copyright as to their contents.
*/
"use strict";
var usr = {};
/**
* binarySearch(a, v, fnCompare)
*
* @param {Array} a is an array
* @param {number|string|Array|Object} v
* @param {function((number|string), (number|string))} [fnCompare]
* @return {number} the index of matching entry if non-negative, otherwise the index of the insertion point
*/
usr.binarySearch = function(a, v, fnCompare) {
var left = 0;
var right = a.length;
var found = 0;
if (fnCompare === undefined) {
fnCompare = function(a, b) {
return a > b? 1 : a < b? -1 : 0;
};
}
while (left < right) {
var middle = (left + right) >> 1;
var compareResult;
compareResult = fnCompare(v, a[middle]);
if (compareResult > 0) {
left = middle + 1;
} else {
right = middle;
found = !compareResult;
}
}
return found? left : ~left;
};
/**
* binaryInsert(a, v, fnCompare)
*
* If element v already exists in array a, the array is unchanged (we don't allow duplicates); otherwise, the
* element is inserted into the array at the appropriate index.
*
* @param {Array} a is an array
* @param {number|string|Array|Object} v is the value to insert
* @param {function((number|string), (number|string))} [fnCompare]
*/
usr.binaryInsert = function(a, v, fnCompare) {
var index = usr.binarySearch(a, v, fnCompare);
if (index < 0) {
a.splice(-(index + 1), 0, v);
}
};
/**
* getTime()
*
* @return {number} the current time, in milliseconds
*/
usr.getTime = function() {
return Date.now() || +new Date();
};
/**
* getTimestamp()
*
* @return {string} timestamp containing the current date and time ("yyyy-mm-dd hh:mm:ss")
*/
usr.getTimestamp = function() {
var date = new Date();
var padNum = function(n) {
return (n < 10? "0" : "") + n;
};
return date.getFullYear() + "-" + padNum(date.getMonth() + 1) + "-" + padNum(date.getDate()) + " " + padNum(date.getHours()) + ":" + padNum(date.getMinutes()) + ":" + padNum(date.getSeconds());
};
/**
* getMonthDays(nMonth, nYear)
*
* Note that if we're being called on behalf of the RTC, its year is always truncated to two digits (mod 100),
* so we have no idea what century the year 0 might refer to. When using the normal leap-year formula, 0 fails
* the mod 100 test but passes the mod 400 test, so as far as the RTC is concerned, every century year is a leap
* year. Since we're most likely dealing with the year 2000, that's fine, since 2000 was also a leap year.
*
* TODO: There IS a separate CMOS byte that's supposed to be set to CMOS_ADDR.CENTURY_DATE; it's always BCD,
* so theoretically it will contain values like 0x19 or 0x20 (for the 20th and 21st centuries, respectively), and
* we could add that as another parameter to this function, to improve the accuracy, but that would go beyond what
* a real RTC actually does.
*
* @param {number} nMonth (1-12)
* @param {number} nYear (normally a 4-digit year, but it may also be mod 100)
* @return {number} the maximum (1-based) day allowed for the specified month and year
*/
usr.getMonthDays = function(nMonth, nYear)
{
var nDays = usr.aMonthDays[nMonth - 1];
if (nDays == 28) {
if ((nYear % 4) === 0 && ((nYear % 100) || (nYear % 400) === 0)) {
nDays++;
}
}
return nDays;
};
usr.asDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
usr.asMonths = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
usr.aMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
/**
* formatDate(sFormat, date)
*
* @param {string} sFormat (eg, "F j, Y", "Y-m-d H:i:s")
* @param {Date} [date] (default is the current time)
* @return {string}
*
* Supported identifiers in sFormat include:
*
* a: lowercase ante meridiem and post meridiem (am or pm)
* d: day of the month, 2 digits with leading zeros (01,...,31)
* g: hour in 12-hour format, without leading zeros (1,...,12)
* i: minutes, with leading zeros (00,...,59)
* j: day of the month, without leading zeros (1,...,31)
* l: day of the week ("Sunday",...,"Saturday")
* m: month, with leading zeros (01,...,12)
* s: seconds, with leading zeros (00,...,59)
* F: month ("January",...,"December")
* H: hour in 24-hour format, with leading zeros (00,...,23)
* Y: year (eg, 2014)
*
* For more inspiration, see: http://php.net/manual/en/function.date.php
*/
usr.formatDate = function(sFormat, date) {
var sDate = "";
if (!date) date = new Date();
var iHour = date.getHours();
var iDay = date.getDate();
var iMonth = date.getMonth() + 1;
for (var i = 0; i < sFormat.length; i++) {
var ch;
switch((ch = sFormat.charAt(i))) {
case 'a':
sDate += (iHour < 12? "am" : "pm");
break;
case 'd':
sDate += ('0' + iDay).slice(-2);
break;
case 'g':
sDate += (!iHour? 12 : (iHour > 12? iHour - 12 : iHour));
break;
case 'i':
sDate += ('0' + date.getMinutes()).slice(-2);
break;
case 'j':
sDate += iDay;
break;
case 'l':
sDate += usr.asDays[date.getDay()];
break;
case 'm':
sDate += ('0' + iMonth).slice(-2);
break;
case 's':
sDate += ('0' + date.getSeconds()).slice(-2);
break;
case 'F':
sDate += usr.asMonths[iMonth - 1];
break;
case 'H':
sDate += ('0' + iHour).slice(-2);
break;
case 'Y':
sDate += date.getFullYear();
break;
default:
sDate += ch;
break;
}
}
return sDate;
};
if (typeof module !== 'undefined') module.exports = usr;

View file

@ -0,0 +1,647 @@
/**
* @fileoverview Browser-related helper functions
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a> (@jeffpar)
* @version 1.0
* Created 2014-05-08
*
* Copyright © 2012-2014 Jeff Parsons <Jeff@pcjs.org>
*
* This file is part of the JavaScript Machines Project (aka JSMachines) at <http://jsmachines.net/>
* and <http://pcjs.org/>.
*
* JSMachines 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.
*
* JSMachines 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 JSMachines.
* If not, see <http://www.gnu.org/licenses/gpl.html>.
*
* You are required to include the above copyright notice in every source code file of every
* copy or modified version of this work, and to display that copyright notice on every screen
* that loads or runs any version of this software (see Computer.sCopyright).
*
* Some JSMachines 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 the
* JSMachines Project for purposes of the GNU General Public License, and the author does not claim
* any copyright as to their contents.
*/
/*
* According to http://www.w3schools.com/jsref/jsref_obj_global.asp, these are the *global* properties
* and functions of JavaScript-in-the-Browser:
*
* Property Description
* ---
* Infinity A numeric value that represents positive/negative infinity
* NaN "Not-a-Number" value
* undefined Indicates that a variable has not been assigned a value
*
* Function Description
* ---
* decodeURI() Decodes a URI
* decodeURIComponent() Decodes a URI component
* encodeURI() Encodes a URI
* encodeURIComponent() Encodes a URI component
* escape() Deprecated in version 1.5. Use encodeURI() or encodeURIComponent() instead
* eval() Evaluates a string and executes it as if it was script code
* isFinite() Determines whether a value is a finite, legal number
* isNaN() Determines whether a value is an illegal number
* Number() Converts an object's value to a number
* parseFloat() Parses a string and returns a floating point number
* parseInt() Parses a string and returns an integer
* String() Converts an object's value to a string
* unescape() Deprecated in version 1.5. Use decodeURI() or decodeURIComponent() instead
*
* And according to http://www.w3schools.com/jsref/obj_window.asp, these are the properties and functions
* of the *window* object.
*
* Property Description
* ---
* closed Returns a Boolean value indicating whether a window has been closed or not
* defaultStatus Sets or returns the default text in the statusbar of a window
* document Returns the Document object for the window (See Document object)
* frames Returns an array of all the frames (including iframes) in the current window
* history Returns the History object for the window (See History object)
* innerHeight Returns the inner height of a window's content area
* innerWidth Returns the inner width of a window's content area
* length Returns the number of frames (including iframes) in a window
* location Returns the Location object for the window (See Location object)
* name Sets or returns the name of a window
* navigator Returns the Navigator object for the window (See Navigator object)
* opener Returns a reference to the window that created the window
* outerHeight Returns the outer height of a window, including toolbars/scrollbars
* outerWidth Returns the outer width of a window, including toolbars/scrollbars
* pageXOffset Returns the pixels the current document has been scrolled (horizontally) from the upper left corner of the window
* pageYOffset Returns the pixels the current document has been scrolled (vertically) from the upper left corner of the window
* parent Returns the parent window of the current window
* screen Returns the Screen object for the window (See Screen object)
* screenLeft Returns the x coordinate of the window relative to the screen
* screenTop Returns the y coordinate of the window relative to the screen
* screenX Returns the x coordinate of the window relative to the screen
* screenY Returns the y coordinate of the window relative to the screen
* self Returns the current window
* status Sets or returns the text in the statusbar of a window
* top Returns the topmost browser window
*
* Method Description
* ---
* alert() Displays an alert box with a message and an OK button
* atob() Decodes a base-64 encoded string
* blur() Removes focus from the current window
* btoa() Encodes a string in base-64
* clearInterval() Clears a timer set with setInterval()
* clearTimeout() Clears a timer set with setTimeout()
* close() Closes the current window
* confirm() Displays a dialog box with a message and an OK and a Cancel button
* createPopup() Creates a pop-up window
* focus() Sets focus to the current window
* moveBy() Moves a window relative to its current position
* moveTo() Moves a window to the specified position
* open() Opens a new browser window
* print() Prints the content of the current window
* prompt() Displays a dialog box that prompts the visitor for input
* resizeBy() Resizes the window by the specified pixels
* resizeTo() Resizes the window to the specified width and height
* scroll() This method has been replaced by the scrollTo() method.
* scrollBy() Scrolls the content by the specified number of pixels
* scrollTo() Scrolls the content to the specified coordinates
* setInterval() Calls a function or evaluates an expression at specified intervals (in milliseconds)
* setTimeout() Calls a function or evaluates an expression after a specified number of milliseconds
* stop() Stops the window from loading
*/
"use strict";
/* global window: true, setTimeout: false, clearTimeout: false, SITEHOST: false */
/*
* We must defer loading the Component module until the function(s) requiring it are
* called; otherwise, we create an initialization cycle in which Component requires weblib
* and weblib requires Component.
*
* In an ideal world, weblib would not be dependent on Component, but we really want to use
* its logging functions.
*/
if (typeof module !== 'undefined') {
var Component;
require("./defines");
var str = require("./strlib");
var ReportAPI = require("./reportapi");
}
var web = {};
/**
* loadResource(sURL, fAsync, data, componentNotify, fnNotify, pNotify)
*
* Request the specified resource (sURL), and once the request is complete,
* optionally call the specified method (fnNotify) of the specified component (componentNotify).
*
* TODO: Figure out how we can strongly type the fnNotify parameter, because the Closure Compiler has issues with:
*
* {function(this:Component, string, (string|null), number, (number|string|null|Object|Array|undefined))} [fnNotify]
*
* @param {string} sURL
* @param {boolean} [fAsync] is true for an asynchronous request
* @param {Object|null} [data] for a POST request (default is a GET request)
* @param {Component} [componentNotify]
* @param {function(...)} [fnNotify]
* @param {number|string|null|Object|Array} [pNotify] optional fnNotify info parameter
* @return {Array} containing errorCode and responseText (empty array if fAsync is true)
*/
web.loadResource = function(sURL, fAsync, data, componentNotify, fnNotify, pNotify)
{
fAsync = !!fAsync; // ensure that fAsync is a valid boolean (Internet Explorer xmlHTTP functions insist on it)
if (typeof module !== 'undefined') {
/*
* We don't even need to load Component, because we can't use any of the code below
* within Node anyway. Instead, we must hand this request off to our network library.
*
* if (!Component) Component = require("./component");
*/
var net = require("./netlib");
return net.loadResource(sURL, fAsync, data, componentNotify, fnNotify, pNotify);
}
var nErrorCode = 0;
var sURLData = null;
var sURLName = str.getBaseName(sURL);
var xmlHTTP = (window.XMLHttpRequest? new window.XMLHttpRequest() : new window.ActiveXObject("Microsoft.XMLHTTP"));
if (fAsync) {
xmlHTTP.onreadystatechange = function() {
if (xmlHTTP.readyState === 4) {
/*
* The following line is recommended for WebKit, as a work-around to prevent the handler firing multiple
* times when debugging. Unfortunately, that's not the only XMLHttpRequest problem that occurs when
* debugging, so I think the WebKit problem is deeper than that. When we have multiple XMLHttpRequests
* pending, any debugging activity means most of them simply get dropped on floor, so what may actually be
* happening are mis-notifications rather than redundant notifications.
*/
xmlHTTP.onreadystatechange = undefined;
sURLData = xmlHTTP.responseText;
if (xmlHTTP.status == 200) {
Component.log("xmlHTTP.onreadystatechange(" + sURL + "): returned " + sURLData.length + " bytes");
}
else {
nErrorCode = xmlHTTP.status || -1;
Component.log("xmlHTTP.onreadystatechange(" + sURL + "): error code " + nErrorCode);
}
if (componentNotify && fnNotify) fnNotify.call(componentNotify, sURLName, sURLData, nErrorCode, pNotify);
}
};
}
if (data) {
var sData = "";
for (var p in data) {
if (!data.hasOwnProperty(p)) continue;
if (sData) sData += "&";
sData += p + '=' + encodeURIComponent(data[p]);
}
sData = sData.replace(/%20/g, '+');
Component.log("web.loadResource(POST " + sURL + "): " + sData.length + " bytes");
xmlHTTP.open("POST", sURL, fAsync);
xmlHTTP.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xmlHTTP.send(sData);
} else {
Component.log("web.loadResource(GET " + sURL + ")");
xmlHTTP.open("GET", sURL, fAsync);
xmlHTTP.send();
}
var response = [];
if (!fAsync) {
sURLData = xmlHTTP.responseText;
if (xmlHTTP.status == 200) {
Component.log("web.loadResource(" + sURL + "): returned " + sURLData.length + " bytes");
} else {
nErrorCode = xmlHTTP.status || -1;
Component.log("web.loadResource(" + sURL + "): error code " + nErrorCode);
}
if (componentNotify && fnNotify) fnNotify.call(componentNotify, sURLName, sURLData, nErrorCode, pNotify);
response = [nErrorCode, sURLData];
}
return response;
};
/**
* sendReport(sApp, sVer, sURL, sUser, sType, sReport, sHostName)
*
* Send a report (eg, bug report) to the server.
*
* @param {string} sApp (eg, "PCjs")
* @param {string} sVer (eg, "1.02")
* @param {string} sURL (eg, "/configs/pc/machines/5150/mda/64kb/index.xml")
* @param {string} sUser (ie, the user key, if any)
* @param {string} sType (eg, "bug"); one of ReportAPI.TYPE.*
* @param {string} sReport (eg, unparsed state data)
* @param {string} [sHostName] (default is http://SITEHOST)
*/
web.sendReport = function(sApp, sVer, sURL, sUser, sType, sReport, sHostName)
{
var data = {};
data[ReportAPI.QUERY.APP] = sApp;
data[ReportAPI.QUERY.VER] = sVer;
data[ReportAPI.QUERY.URL] = sURL;
data[ReportAPI.QUERY.USER] = sUser;
data[ReportAPI.QUERY.TYPE] = sType;
data[ReportAPI.QUERY.DATA] = sReport;
var sReportURL = (sHostName? sHostName : "http://" + SITEHOST) + ReportAPI.ENDPOINT;
web.loadResource(sReportURL, true, data);
};
/**
* getHost()
*
* @return {string}
*/
web.getHost = function()
{
return ("http://" + (window? window.location.host : SITEHOST));
};
/**
* getHostURL()
*
* @return {string|null}
*/
web.getHostURL = function()
{
return (window? window.location.href : null);
};
/**
* getUserAgent()
*
* @return {string}
*/
web.getUserAgent = function()
{
return (window? window.navigator.userAgent : "");
};
/**
* alertUser(sMessage)
*
* @param {string} sMessage
*/
web.alertUser = function(sMessage)
{
if (window) {
window.alert(sMessage);
} else {
console.log(sMessage);
}
};
/**
* confirmUser(sPrompt)
*
* @param {string} sPrompt
* @returns {boolean} true if the user clicked OK, false if Cancel/Close
*/
web.confirmUser = function(sPrompt)
{
var fResponse = false;
if (window) {
fResponse = window.confirm(sPrompt);
}
return fResponse;
};
/**
* promptUser()
*
* @param {string} sPrompt
* @param {string} [sDefault]
* @returns {string|null}
*/
web.promptUser = function(sPrompt, sDefault)
{
var sResponse = null;
if (window) {
sResponse = window.prompt(sPrompt, sDefault === undefined? "" : sDefault);
}
return sResponse;
};
/**
* getLocalStorageItem(sKey)
*
* Returns the requested key value, or null if the key does not exist, or undefined if localStorage is not available
*
* @param {string} sKey
* @return {string|null|undefined} sValue
*/
web.getLocalStorageItem = function(sKey)
{
var sValue;
if (window) {
sValue = window.localStorage.getItem(sKey);
}
return sValue;
};
/**
* setLocalStorageItem(sKey, sValue)
*
* @param {string} sKey
* @param {string} sValue
* return {boolean} true if localStorage is available, false if not
*/
web.setLocalStorageItem = function(sKey, sValue)
{
if (window) {
window.localStorage.setItem(sKey, sValue);
return true;
}
return false;
};
/**
* reloadPage()
*/
web.reloadPage = function()
{
if (window) window.location.reload();
};
/**
* isUserAgent(s)
*
* Check the browser's user-agent string for the given substring; "iOS" and "MSIE" are special values you can
* use that will match any iOS or MSIE browser, respectively (even IE11, in the case of "MSIE").
*
* 2013-11-06: In a questionable move, MSFT changed the user-agent reported by IE11 on Windows 8.1, eliminating
* the "MSIE" string (which MSDN calls a "version token"; see http://msdn.microsoft.com/library/ms537503.aspx);
* they say "public websites should rely on feature detection, rather than browser detection, in order to design
* their sites for browsers that don't support the features used by the website." So, in IE11, we get a user-agent
* that tries to fool apps into thinking the browser is more like WebKit or Gecko:
*
* Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko
*
* That's a nice idea, but in the meantime, they hosed the XSL transform code in embed.js, which contained
* some very critical browser-specific code; turning on IE's "Compatibility Mode" didn't help either, because
* that's a sledgehammer solution which restores the old user-agent string but also disables other features like
* HTML5 canvas support. As an interim solution, I'm treating any "MSIE" check as a check for either "MSIE" or
* "Trident".
*
* UPDATE: I've since found ways to make the code in embed.js more browser-agnostic, so for now, there's isn't
* any code that cares about "MSIE", but I've left the change in place, because I wouldn't be surprised if I'll
* need more IE-specific code in the future, perhaps for things like copy/paste functionality, or mouse capture.
*
* @param {string} s is a substring to search for in the user-agent; as noted above, "iOS" and "MSIE" are special values
* @return {boolean} is true if the string was found, false if not
*/
web.isUserAgent = function(s)
{
if (window) {
var userAgent = web.getUserAgent();
/*
* Here's one case where we have to be careful with Component, because when isUserAgent() is called by
* the init code below, component.js hasn't been loaded yet. The simplest solution is to remove the call.
*
* if (Component) Component.log("agent: " + userAgent);
*
* And yes, it would be pointless to use the conditional (?) operator below, if not for the Google Closure
* Compiler (v20130823) failing to detect the entire expression as a boolean.
*/
return (s == "iOS" && userAgent.match(/(iPod|iPhone|iPad)/) && userAgent.match(/AppleWebKit/) || s == "MSIE" && userAgent.match(/(MSIE|Trident)/) || (userAgent.indexOf(s) >= 0))? true : false;
}
return false;
};
/**
* getURLParameters(sParms)
*
* @param {string} [sParms] containing the parameter portion of a URL (ie, after the '?')
* @return {Object} containing properties for each parameter found
*/
web.getURLParameters = function(sParms)
{
var aParms = {};
if (window) { // an alternative to "if (typeof module === 'undefined')" if require("defines") has been invoked
if (!sParms) {
/*
* Note that window.location.href returns the entire URL, whereas window.location.search returns
* only the parameters, if any (starting with the '?', which we skip over with a substr() call).
*/
sParms = window.location.search.substr(1);
}
var match;
var pl = /\+/g; // RegExp for replacing addition symbol with a space
var search = /([^&=]+)=?([^&]*)/g;
var decode = function(s) { return decodeURIComponent(s.replace(pl, " ")); };
while ((match = search.exec(sParms))) {
aParms[decode(match[1])] = decode(match[2]);
}
}
return aParms;
};
/**
* onCountRepeat(n, fn, fnComplete, msDelay)
*
* Call fn() n times with an msDelay millisecond delay between calls, then
* call fnComplete() when the count has been exhausted OR fn() returns false.
*
* @param {number} n
* @param {function()} fn
* @param {function()} fnComplete
* @param {number} [msDelay]
*/
web.onCountRepeat = function(n, fn, fnComplete, msDelay)
{
var fnRepeat = function doCountRepeat() {
n -= 1;
if (n >= 0) {
if (!fn()) n = 0;
}
if (n > 0) {
setTimeout(fnRepeat, msDelay || 0);
return;
}
fnComplete();
};
fnRepeat();
};
/**
* onClickRepeat(e, msDelay, msRepeat, fn)
*
* Repeatedly call fn() with an initial msDelay, and an msRepeat delay thereafter,
* as long as HTML control Object e has an active "down" event and fn() returns true.
*
* @param {Object} e
* @param {number} msDelay
* @param {number} msRepeat
* @param {function(boolean)} fn is passed false on the first call, true on all repeated calls
*/
web.onClickRepeat = function(e, msDelay, msRepeat, fn)
{
var ms = 0, timer = null, fIgnoreMouseEvents = false;
var fnRepeat = function doClickRepeat() {
if (fn(ms === msRepeat)) {
timer = setTimeout(fnRepeat, ms);
ms = msRepeat;
}
};
e.onmousedown = function() {
// Component.println("onMouseDown()");
if (!fIgnoreMouseEvents) {
if (!timer) {
ms = msDelay;
fnRepeat();
}
}
};
e.ontouchstart = function() {
// Component.println("onTouchStart()");
if (!timer) {
ms = msDelay;
fnRepeat();
}
};
e.onmouseup = e.onmouseout = function() {
// Component.println("onMouseUp()/onMouseOut()");
if (timer) {
clearTimeout(timer);
timer = null;
}
};
e.ontouchend = e.ontouchcancel = function() {
// Component.println("onTouchEnd()/onTouchCancel()");
if (timer) {
clearTimeout(timer);
timer = null;
}
/*
* Devices that generate ontouch* events ALSO generate onmouse* events,
* and generally do so immediately after all the touch events are complete,
* so unless we want double the action, we need to ignore mouse events.
*/
fIgnoreMouseEvents = true;
};
};
web.aPageEventHandlers = {
'init': [], // list of window 'onload' handlers
'show': [], // list of window 'onpageshow' handlers
'exit': [] // list of window 'onunload' handlers (although we prefer to use 'onbeforeunload' if possible)
};
web.fPageEventsEnabled = true;
/**
* onPageEvent(sName, fn)
*
* @param {string} sFunc
* @param {function()} fn
*
* Use this instead of setting window['onunload'], window['onunload'], etc.
* Allows multiple JavaScript modules to define a handler for the same event.
*
* Moreover, it's risky to refer to obscure event handlers with "dot" names, because
* the Closure Compiler may erroneously replace them (eg, window.onpageshow is a good example).
*/
web.onPageEvent = function(sFunc, fn)
{
if (window) {
var fnPrev = window[sFunc];
if (typeof fnPrev !== 'function') {
window[sFunc] = fn;
} else {
/*
* TODO: Determine whether there's any value in receiving/sending the Event object that the
* browser provides when it generates the original event.
*/
window[sFunc] = function onWindowEvent() {
if (fnPrev) fnPrev();
fn();
};
}
}
};
/**
* onInit(fn)
*
* @param {function()} fn
*
* Use this instead of setting window.onload. Allows multiple JavaScript modules to define their own 'onload' event handler.
*/
web.onInit = function(fn)
{
web.aPageEventHandlers['init'].push(fn);
};
/**
* onShow(fn)
*
* @param {function()} fn
*
* Use this instead of setting window.onpageshow. Allows multiple JavaScript modules to define their own 'onpageshow' event handler.
*/
web.onShow = function(fn)
{
web.aPageEventHandlers['show'].push(fn);
};
/**
* onExit(fn)
*
* @param {function()} fn
*
* Use this instead of setting window.onunload. Allows multiple JavaScript modules to define their own 'onunload' event handler.
*/
web.onExit = function(fn)
{
web.aPageEventHandlers['exit'].push(fn);
};
/**
* doPageEvent(afn)
*
* @param {Array.<function()>} afn
*/
web.doPageEvent = function(afn)
{
if (web.fPageEventsEnabled) {
for (var i = 0; i < afn.length; i++) {
afn[i]();
}
}
};
/**
* enablePageEvents(fEnable)
*
* @param {boolean} fEnable is true to enable page events, false to disable (they're enabled by default)
*/
web.enablePageEvents = function(fEnable)
{
web.fPageEventsEnabled = fEnable;
};
/**
* sendPageEvent(sEvent)
*
* This allows us to manually trigger page events.
*
* @param {string} sEvent (one of 'init', 'show' or 'exit')
*/
web.sendPageEvent = function(sEvent)
{
if (web.aPageEventHandlers[sEvent]) {
web.doPageEvent(web.aPageEventHandlers[sEvent]);
}
};
web.onPageEvent('onload', function onPageLoad() { web.doPageEvent(web.aPageEventHandlers['init']); });
web.onPageEvent('onpageshow', function onPageShow() { web.doPageEvent(web.aPageEventHandlers['show']); });
web.onPageEvent(web.isUserAgent("Opera") || web.isUserAgent("iOS")? 'onunload' : 'onbeforeunload', function onPageUnload() { web.doPageEvent(web.aPageEventHandlers['exit']); });
if (typeof module !== 'undefined') module.exports = web;