Unforked the shared modules -- ES6 is now the default

This commit is contained in:
Jeff Parsons 2017-01-31 13:50:55 -08:00 committed by Jeff Parsons
commit 822ab39cbe
108 changed files with 3310 additions and 9776 deletions

File diff suppressed because it is too large Load diff

View file

@ -28,11 +28,9 @@
"use strict";
if (DEBUGGER) {
if (NODE) {
var str = require("../../shared/lib/strlib");
var Component = require("../../shared/lib/component");
}
if (NODE) {
var Str = require("../../shared/lib/strlib");
var Component = require("../../shared/lib/component");
}
/**
@ -50,105 +48,102 @@ if (DEBUGGER) {
* fTemporary:(boolean|undefined),
* sCmd:(string|undefined),
* aCmds:(Array.<string>|undefined)
* }} DbgAddr
* }}
*/
var DbgAddr;
/**
* Debugger(parmsDbg)
* Since the Closure Compiler treats ES6 classes as @struct rather than @dict by default,
* it deters us from defining named properties on our components; eg:
*
* The Debugger component supports the following optional (parmsDbg) properties:
* this['exports'] = {...}
*
* base: the base to use for most numeric input/output (default is 16)
* results in an error:
*
* The Debugger component is a shared component containing a subset of functionality used by
* the other CPU-specific Debuggers (eg, DebuggerX86). Over time, the goal is to factor out as
* much common debugging support as possible from those components into this one.
* Cannot do '[]' access on a struct
*
* @constructor
* @extends Component
* @param {Object} parmsDbg
* So, in order to define 'exports', we must override the @struct assumption by annotating
* the class as @unrestricted (or @dict). Note that this must be done both here and in the
* subclass (eg, SerialPort), because otherwise the Compiler won't allow us to *reference*
* the named property either.
*
* TODO: Consider marking ALL our classes unrestricted, because otherwise it forces us to
* define every single property the class uses in its constructor, which results in a fair
* bit of redundant initialization, since many properties aren't (and don't need to be) fully
* initialized until the appropriate init(), reset(), restore(), etc. function is called.
*
* The upside, however, may be that since the structure of the class is completely defined by
* the constructor, JavaScript engines may be able to optimize and run more efficiently.
*
* @unrestricted
*/
function Debugger(parmsDbg)
{
if (DEBUGGER) {
class Debugger extends Component {
/**
* Debugger(parmsDbg)
*
* The Debugger component supports the following optional (parmsDbg) properties:
*
* base: the base to use for most numeric input/output (default is 16)
*
* The Debugger component is a shared component containing a subset of functionality used by
* the other CPU-specific Debuggers (eg, DebuggerX86). Over time, the goal is to factor out as
* much common debugging support as possible from those components into this one.
*
* @param {Object} parmsDbg
*/
constructor(parmsDbg) {
if (DEBUGGER) {
Component.call(this, "Debugger", parmsDbg, Debugger);
super("Debugger", parmsDbg);
/*
* Default base used to display all values; modified with the "s base" command.
*/
this.nBase = parmsDbg['base'] || 16;
this.fParens = false;
/*
* Default base used to display all values; modified with the "s base" command.
*/
this.nBase = +parmsDbg['base'] || 16;
this.fParens = false;
/*
* These keep track of instruction activity, but only when tracing or when Debugger checks
* have been enabled (eg, one or more breakpoints have been set).
*
* They are zeroed by the reset() notification handler. cInstructions is advanced by
* stepCPU() and checkInstruction() calls. nCycles is updated by every stepCPU() or stop()
* call and simply represents the number of cycles performed by the last run of instructions.
*/
this.nCycles = 0;
this.cOpcodes = this.cOpcodesStart = 0;
/*
* These keep track of instruction activity, but only when tracing or when Debugger checks
* have been enabled (eg, one or more breakpoints have been set).
*
* They are zeroed by the reset() notification handler. cInstructions is advanced by
* stepCPU() and checkInstruction() calls. nCycles is updated by every stepCPU() or stop()
* call and simply represents the number of cycles performed by the last run of instructions.
*/
this.nCycles = 0;
this.cOpcodes = this.cOpcodesStart = 0;
/*
* fAssemble is true when "assemble mode" is active, false when not.
*/
this.fAssemble = false;
/*
* fAssemble is true when "assemble mode" is active, false when not.
*/
this.fAssemble = false;
/*
* This maintains command history. New commands are inserted at index 0 of the array.
* When Enter is pressed on an empty input buffer, we default to the command at aPrevCmds[0].
*/
this.iPrevCmd = -1;
this.aPrevCmds = [];
/*
* This maintains command history. New commands are inserted at index 0 of the array.
* When Enter is pressed on an empty input buffer, we default to the command at aPrevCmds[0].
*/
this.iPrevCmd = -1;
this.aPrevCmds = [];
/*
* aVariables is an object with properties that grow as setVariable() assigns more variables;
* each property corresponds to one variable, where the property name is the variable name (ie,
* a string beginning with a letter or underscore, followed by zero or more additional letters,
* digits, or underscores) and the property value is the variable's numeric value. See doVar()
* and setVariable() for details.
*
* Note that parseValue() parses variables before numbers, so any variable that looks like a
* unprefixed hex value (eg, "a5" as opposed to "0xa5") will trump the numeric value. Unprefixed
* hex values are a convenience of parseValue(), which always calls str.parseInt() with a default
* base of 16; however, that default be overridden with a variety of explicit prefixes or suffixes
* (eg, a leading "0o" to indicate octal, a trailing period to indicate decimal, etc.)
*
* See str.parseInt() for more details about supported numbers.
*/
this.aVariables = {};
/*
* aVariables is an object with properties that grow as setVariable() assigns more variables;
* each property corresponds to one variable, where the property name is the variable name (ie,
* a string beginning with a letter or underscore, followed by zero or more additional letters,
* digits, or underscores) and the property value is the variable's numeric value. See doVar()
* and setVariable() for details.
*
* Note that parseValue() parses variables before numbers, so any variable that looks like a
* unprefixed hex value (eg, "a5" as opposed to "0xa5") will trump the numeric value. Unprefixed
* hex values are a convenience of parseValue(), which always calls Str.parseInt() with a default
* base of 16; however, that default be overridden with a variety of explicit prefixes or suffixes
* (eg, a leading "0o" to indicate octal, a trailing period to indicate decimal, etc.)
*
* See Str.parseInt() for more details about supported numbers.
*/
this.aVariables = {};
} // endif DEBUGGER
}
if (DEBUGGER) {
Component.subclass(Debugger);
Debugger.aBinOpPrecedence = {
'||': 0, // logical OR
'&&': 1, // logical AND
'|': 2, // bitwise OR
'^': 3, // bitwise XOR
'&': 4, // bitwise AND
'!=': 5, // inequality
'==': 5, // equality
'>=': 6, // greater than or equal to
'>': 6, // greater than
'<=': 6, // less than or equal to
'<': 6, // less than
'>>>': 7, // unsigned bitwise right shift
'>>': 7, // bitwise right shift
'<<': 7, // bitwise left shift
'-': 8, // subtraction
'+': 8, // addition
'%': 9, // remainder
'/': 9, // division
'*': 9 // multiplication
};
} // endif DEBUGGER
}
/**
* getRegIndex(sReg, off)
@ -160,10 +155,9 @@ if (DEBUGGER) {
* @param {number} [off] optional offset into sReg
* @return {number} register index, or -1 if not found
*/
Debugger.prototype.getRegIndex = function(sReg, off)
{
getRegIndex(sReg, off) {
return -1;
};
}
/**
* getRegValue(iReg)
@ -174,10 +168,9 @@ if (DEBUGGER) {
* @param {number} iReg
* @return {number|undefined}
*/
Debugger.prototype.getRegValue = function(iReg)
{
getRegValue(iReg) {
return undefined;
};
}
/**
* parseAddrReference(s, sAddr)
@ -191,10 +184,9 @@ if (DEBUGGER) {
* @param {string} sAddr
* @return {string}
*/
Debugger.prototype.parseAddrReference = function(s, sAddr)
{
parseAddrReference(s, sAddr) {
return s.replace('[' + sAddr + ']', "unimplemented");
};
}
/**
* getNextCommand()
@ -202,8 +194,7 @@ if (DEBUGGER) {
* @this {Debugger}
* @return {string}
*/
Debugger.prototype.getNextCommand = function()
{
getNextCommand() {
var sCmd;
if (this.iPrevCmd > 0) {
sCmd = this.aPrevCmds[--this.iPrevCmd];
@ -212,7 +203,7 @@ if (DEBUGGER) {
this.iPrevCmd = -1;
}
return sCmd;
};
}
/**
* getPrevCommand()
@ -220,14 +211,13 @@ if (DEBUGGER) {
* @this {Debugger}
* @return {string|null}
*/
Debugger.prototype.getPrevCommand = function()
{
getPrevCommand() {
var sCmd = null;
if (this.iPrevCmd < this.aPrevCmds.length - 1) {
sCmd = this.aPrevCmds[++this.iPrevCmd];
}
return sCmd;
};
}
/**
* parseCommand(sCmd, fSave, chSep)
@ -238,8 +228,7 @@ if (DEBUGGER) {
* @param {string} [chSep] is the command separator character (default is ';')
* @return {Array.<string>}
*/
Debugger.prototype.parseCommand = function(sCmd, fSave, chSep)
{
parseCommand(sCmd, fSave, chSep) {
if (fSave) {
if (!sCmd) {
if (this.fAssemble) {
@ -265,7 +254,7 @@ if (DEBUGGER) {
* associated with a breakpoint), we can no longer perform simplistic splitting.
*
* a = sCmd.split(chSep || ';');
* for (var i = 0; i < a.length; i++) a[i] = str.trim(a[i]);
* for (var i = 0; i < a.length; i++) a[i] = Str.trim(a[i]);
*
* We may now split on semi-colons ONLY if they are outside a quoted sequence.
*
@ -298,13 +287,13 @@ if (DEBUGGER) {
* Recall that substring() accepts starting (inclusive) and ending (exclusive)
* indexes, whereas substr() accepts a starting index and a length. We need the former.
*/
a.push(str.trim(sCmd.substring(iPrev, i)));
a.push(Str.trim(sCmd.substring(iPrev, i)));
iPrev = i + 1;
}
}
}
return a;
};
}
/**
* evalExpression(aVals, aOps, cOps)
@ -327,8 +316,7 @@ if (DEBUGGER) {
* @param {number} [cOps] (default is all)
* @return {boolean} true if successful, false if error
*/
Debugger.prototype.evalExpression = function(aVals, aOps, cOps)
{
evalExpression(aVals, aOps, cOps) {
cOps = cOps || -1;
while (cOps-- && aOps.length) {
var chOp = aOps.pop();
@ -402,7 +390,7 @@ if (DEBUGGER) {
aVals.push(valNew|0);
}
return true;
};
}
/**
* parseExpression(sExp, fPrint)
@ -440,8 +428,7 @@ if (DEBUGGER) {
* @param {boolean} [fPrint] is true to print all resolved values, false for quiet parsing
* @return {number|undefined} numeric value, or undefined if sExp contains any undefined or invalid values
*/
Debugger.prototype.parseExpression = function(sExp, fPrint)
{
parseExpression(sExp, fPrint) {
var value;
if (sExp) {
@ -468,7 +455,7 @@ if (DEBUGGER) {
while (i < asValues.length) {
var sValue = asValues[i++];
var cchValue = sValue.length;
var s = str.trim(sValue);
var s = Str.trim(sValue);
if (!s) {
fError = true;
break;
@ -500,7 +487,7 @@ if (DEBUGGER) {
}
}
return value;
};
}
/**
* parseReference(s)
@ -514,8 +501,7 @@ if (DEBUGGER) {
* @param {string} s
* @return {string}
*/
Debugger.prototype.parseReference = function(s)
{
parseReference(s) {
var a;
var chOpen = this.fParens? '(' : '{';
var chClose = this.fParens? ')' : '}';
@ -530,7 +516,7 @@ if (DEBUGGER) {
s = this.parseAddrReference(s, a[1]);
}
return this.parseSysVars(s);
};
}
/**
* parseSysVars(s)
@ -543,8 +529,7 @@ if (DEBUGGER) {
* @param {string} s
* @return {string}
*/
Debugger.prototype.parseSysVars = function(s)
{
parseSysVars(s) {
var a;
while (a = s.match(/\$([a-z]+)/i)) {
var v = null;
@ -557,7 +542,7 @@ if (DEBUGGER) {
s = s.replace(a[0], v.toString());
}
return s;
};
}
/**
* parseValue(sValue, sName, fQuiet)
@ -568,8 +553,7 @@ if (DEBUGGER) {
* @param {boolean} [fQuiet]
* @return {number|undefined} numeric value, or undefined if sValue is either undefined or invalid
*/
Debugger.prototype.parseValue = function(sValue, sName, fQuiet)
{
parseValue(sValue, sName, fQuiet) {
var value;
if (sValue != null) {
var iReg = this.getRegIndex(sValue);
@ -578,7 +562,7 @@ if (DEBUGGER) {
} else {
value = this.getVariable(sValue);
if (value == null) {
value = str.parseInt(sValue, this.nBase);
value = Str.parseInt(sValue, this.nBase);
}
}
if (value == null && !fQuiet) this.println("invalid " + (sName? sName : "value") + ": " + sValue);
@ -586,7 +570,7 @@ if (DEBUGGER) {
if (!fQuiet) this.println("missing " + (sName || "value"));
}
return value;
};
}
/**
* printValue(sVar, value)
@ -596,13 +580,12 @@ if (DEBUGGER) {
* @param {number|undefined} value
* @return {boolean} true if value defined, false if not
*/
Debugger.prototype.printValue = function(sVar, value)
{
printValue(sVar, value) {
var sValue;
var fDefined = false;
if (value !== undefined) {
fDefined = true;
sValue = str.toHex(value, 0, true) + " " + value + ". " + str.toOct(value, 0, true) + " " + str.toBinBytes(value, 4, true);
sValue = Str.toHex(value, 0, true) + " " + value + ". " + Str.toOct(value, 0, true) + " " + Str.toBinBytes(value, 4, true);
if (value >= 0x20 && value < 0x7F) {
sValue += " '" + String.fromCharCode(value) + "'";
}
@ -610,7 +593,7 @@ if (DEBUGGER) {
sVar = (sVar != null? (sVar + ": ") : "");
this.println(sVar + sValue);
return fDefined;
};
}
/**
* printVariable(sVar)
@ -619,8 +602,7 @@ if (DEBUGGER) {
* @param {string} [sVar]
* @return {boolean} true if all value(s) defined, false if not
*/
Debugger.prototype.printVariable = function(sVar)
{
printVariable(sVar) {
if (sVar) {
return this.printValue(sVar, this.aVariables[sVar]);
}
@ -630,7 +612,7 @@ if (DEBUGGER) {
cVariables++;
}
return cVariables > 0;
};
}
/**
* delVariable(sVar)
@ -638,10 +620,9 @@ if (DEBUGGER) {
* @this {Debugger}
* @param {string} sVar
*/
Debugger.prototype.delVariable = function(sVar)
{
delVariable(sVar) {
delete this.aVariables[sVar];
};
}
/**
* getVariable(sVar)
@ -650,10 +631,9 @@ if (DEBUGGER) {
* @param {string} sVar
* @return {number|undefined}
*/
Debugger.prototype.getVariable = function(sVar)
{
getVariable(sVar) {
return this.aVariables[sVar];
};
}
/**
* setVariable(sVar, value)
@ -662,15 +642,14 @@ if (DEBUGGER) {
* @param {string} sVar
* @param {number} value
*/
Debugger.prototype.setVariable = function(sVar, value)
{
setVariable(sVar, value) {
this.aVariables[sVar] = value;
};
}
/**
* toStrBase(n, nBytes, fStripLeadingZeros)
*
* Use this instead of str.toHex() or str.toOct() to convert bytes/words to the Debugger's default base.
* Use this instead of Str.toHex() or Str.toOct() to convert bytes/words to the Debugger's default base.
*
* @this {Debugger}
* @param {number|null|undefined} n
@ -678,22 +657,46 @@ if (DEBUGGER) {
* @param {boolean} [fStripLeadingZeros]
* @return {string}
*/
Debugger.prototype.toStrBase = function(n, nBytes, fStripLeadingZeros)
{
toStrBase(n, nBytes, fStripLeadingZeros) {
var s;
switch(this.nBase) {
case 8:
s = str.toOct(n, nBytes * 3 - (nBytes > 2? 1 : 0));
s = Str.toOct(n, nBytes * 3 - (nBytes > 2? 1 : 0));
break;
case 10:
s = n.toString();
break;
case 16:
default:
s = str.toHex(n, nBytes * 2);
s = Str.toHex(n, nBytes * 2);
break;
}
return (fStripLeadingZeros? str.stripLeadingZeros(s) : s);
return (fStripLeadingZeros? Str.stripLeadingZeros(s) : s);
}
}
if (DEBUGGER) {
Debugger.aBinOpPrecedence = {
'||': 0, // logical OR
'&&': 1, // logical AND
'|': 2, // bitwise OR
'^': 3, // bitwise XOR
'&': 4, // bitwise AND
'!=': 5, // inequality
'==': 5, // equality
'>=': 6, // greater than or equal to
'>': 6, // greater than
'<=': 6, // less than or equal to
'<': 6, // less than
'>>>': 7, // unsigned bitwise right shift
'>>': 7, // bitwise right shift
'<<': 7, // bitwise left shift
'-': 8, // subtraction
'+': 8, // addition
'%': 9, // remainder
'/': 9, // division
'*': 9 // multiplication
};
} // endif DEBUGGER

View file

@ -32,9 +32,13 @@
* @define {string}
*/
var APPVERSION = "1.x.x"; // this @define is overridden by the Closure Compiler with the version in package.json
var XMLVERSION = null; // this is set in non-COMPILED builds by embedMachine() if a version number was found in the machine XML
var COPYRIGHT = "Copyright © 2012-2017 Jeff Parsons <Jeff@pcjs.org>";
var LICENSE = "License: GPL version 3 or later <http://gnu.org/licenses/gpl.html>";
var CSSCLASS = "pcjs";
/**

View file

@ -69,36 +69,33 @@ var DiskAPI = {
};
/*
* Common (supported) disk formats
* Common (supported) diskette formats
*
* Each entry in DISK_FORMATS begins with an array of three "CHS" values:
* For no particular reason that I can recall, each entry in DISK_FORMATS is an array of values in "CHS" order:
*
* [# cylinders, # heads, # sectors/track, # bytes/sector, media type]
*
* The 4th value is optional; if omitted, the sector size is assumed to be 512 bytes. The order of these geometric
* values mirrors the structure of our JSON-encoded disk images, which consist of an array of cylinders, each of which
* is an array of heads, each of which is an array of sector objects.
*
* The 5th value is also optional and is used only with PC-DOS diskettes, to help us verify that the logical format
* matches the physical format; it should be one of the values in DiskAPI.FAT. TODO: Actually use the DiskAPI.FAT values.
* If the 4th value is omitted, the sector size is assumed to be 512. The order of these "geometric" values mirrors
* the structure of our JSON-encoded disk images, which consist of an array of cylinders, each of which is an array of
* heads, each of which is an array of sector objects.
*/
DiskAPI.DISK_FORMATS = {
163840: [ 40,1, 8,,0xFE], // media type 0xFE: 40 cylinders, 1 head (single-sided), 8 sectors/track, ( 320 total sectors x 512 bytes/sector == 163840)
184320: [ 40,1, 9,,0xFC], // media type 0xFC: 40 cylinders, 1 head (single-sided), 9 sectors/track, ( 360 total sectors x 512 bytes/sector == 184320)
327680: [ 40,2, 8,,0xFF], // media type 0xFF: 40 cylinders, 2 heads (double-sided), 8 sectors/track, ( 640 total sectors x 512 bytes/sector == 327680)
368640: [ 40,2, 9,,0xFD], // media type 0xFD: 40 cylinders, 2 heads (double-sided), 9 sectors/track, ( 720 total sectors x 512 bytes/sector == 368640)
737280: [ 80,2, 9,,0xF9], // media type 0xF9: 80 cylinders, 2 heads (double-sided), 9 sectors/track, (1440 total sectors x 512 bytes/sector == 737280)
1228800: [ 80,2,15,,0xF9], // media type 0xF9: 80 cylinders, 2 heads (double-sided), 15 sectors/track, (2400 total sectors x 512 bytes/sector == 1228800)
1474560: [ 80,2,18,,0xF0], // media type 0xF0: 80 cylinders, 2 heads (double-sided), 18 sectors/track, (2880 total sectors x 512 bytes/sector == 1474560)
2949120: [ 80,2,36,,0xF0], // media type 0xF0: 80 cylinders, 2 heads (double-sided), 36 sectors/track, (5760 total sectors x 512 bytes/sector == 2949120)
163840: [40,1,8,,0xFE], // media type 0xFE: 40 cylinders, 1 head (single-sided), 8 sectors/track, ( 320 total sectors x 512 bytes/sector == 163840)
184320: [40,1,9,,0xFC], // media type 0xFC: 40 cylinders, 1 head (single-sided), 9 sectors/track, ( 360 total sectors x 512 bytes/sector == 184320)
327680: [40,2,8,,0xFF], // media type 0xFF: 40 cylinders, 2 heads (double-sided), 8 sectors/track, ( 640 total sectors x 512 bytes/sector == 327680)
368640: [40,2,9,,0xFD], // media type 0xFD: 40 cylinders, 2 heads (double-sided), 9 sectors/track, ( 720 total sectors x 512 bytes/sector == 368640)
737280: [80,2,9,,0xF9], // media type 0xF9: 80 cylinders, 2 heads (double-sided), 9 sectors/track, (1440 total sectors x 512 bytes/sector == 737280)
1228800: [80,2,15,,0xF9], // media type 0xF9: 80 cylinders, 2 heads (double-sided), 15 sectors/track, (2400 total sectors x 512 bytes/sector == 1228800)
1474560: [80,2,18,,0xF0], // media type 0xF0: 80 cylinders, 2 heads (double-sided), 18 sectors/track, (2880 total sectors x 512 bytes/sector == 1474560)
2949120: [80,2,36,,0xF0], // media type 0xF0: 80 cylinders, 2 heads (double-sided), 36 sectors/track, (5760 total sectors x 512 bytes/sector == 2949120)
/*
* The following are common early hard drive sizes, which we explicitly map to CHS values, since the BPB can mislead us when attempting to calculate total cylinders.
* The following are common early hard drive sizes, which we explicitly map to CHS values, since the BPB can mislead us when attempting to calculate total cylinders
*/
21368320:[615,4,17], // PC AT 20Mb hard drive (type 2)
/*
* Assorted DEC disk pack formats.
*/
2494464: [203,2,12,512], // RK03 single-platter disk cartridge: 203 tracks, 2 heads, 12 sectors/track, 512 bytes/sector, for a total of 2494464 bytes
2494464: [203,2,12,512], // RK03 single-platter disk cartridge: 203 tracks, 2 heads, 12 sectors/track, 512 bytes/sector, for a total of 2494464 bytes
5242880: [256,2,40,256], // RL01K single-platter disk cartridge: 256 tracks, 2 heads, 40 sectors/track, 256 bytes/sector, for a total of 5242880 bytes
10485760:[512,2,40,256] // RL02K single-platter disk cartridge: 512 tracks, 2 heads, 40 sectors/track, 256 bytes/sector, for a total of 10485760 bytes
};

View file

@ -28,12 +28,10 @@
"use strict";
/* global document: true, window: true, XSLTProcessor: false, APPNAME: false, APPVERSION: false, XMLVERSION: true, DEBUG: true */
if (NODE) {
var Component = require("./component");
var str = require("./strlib");
var web = require("./weblib");
var Str = require("../../shared/lib/strlib");
var Web = require("../../shared/lib/weblib");
var Component = require("../../shared/lib/component");
}
/*
@ -45,8 +43,8 @@ if (NODE) {
* machine component init() handlers.
*
* Also, to prevent those init() handlers from running prematurely, we must disable all page
* notification events at the start of the embedding process (web.enablePageEvents(false)) and
* re-enable them at the end (web.enablePageEvents(true)).
* notification events at the start of the embedding process (Web.enablePageEvents(false)) and
* re-enable them at the end (Web.enablePageEvents(true)).
*/
var fAsync = true;
var cAsyncMachines = 0;
@ -95,7 +93,7 @@ function loadXML(sXMLFile, idMachine, sAppName, sAppClass, sParms, fResolve, dis
parseXML(sXML, sXMLFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done);
};
display("Loading " + sXMLFile + "...");
web.getResource(sXMLFile, null, fAsync, doneLoadXML);
Web.getResource(sXMLFile, null, fAsync, doneLoadXML);
}
/**
@ -171,9 +169,11 @@ function parseXML(sXML, sXMLFile, idMachine, sAppName, sAppClass, sParms, fResol
/*
* Non-COMPILED kludge to replace the version number template in the XSL file (which we assume we're reading,
* since fResolve is false) with whatever XMLVERSION we extracted from the XML file (see corresponding kludge below).
*
* ES6 ALERT: Template strings.
*/
if (!COMPILED && XMLVERSION) {
sXML = sXML.replace(/<xsl:variable name="APPVERSION">1.x.x<\/xsl:variable>/, '<xsl:variable name="APPVERSION">' + XMLVERSION + '</xsl:variable>');
sXML = sXML.replace(/<xsl:variable name="APPVERSION">1.x.x<\/xsl:variable>/, `<xsl:variable name="APPVERSION">${XMLVERSION}</xsl:variable>`);
}
}
@ -188,7 +188,7 @@ function parseXML(sXML, sXMLFile, idMachine, sAppName, sAppClass, sParms, fResol
* 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.getResource() didn't drop any error information on the floor)
* The best I can do at this stage (assuming Web.getResource() didn't drop any error information on the floor)
* is verify that the requested resource "looks like" valid XML (in other words, it begins with a '<').
*/
var xmlDoc = null;
@ -320,7 +320,7 @@ function resolveXML(sXML, display, done)
};
display("Loading " + sRefFile + "...");
web.getResource(sRefFile, null, fAsync, doneReadXML);
Web.getResource(sRefFile, null, fAsync, doneReadXML);
return;
}
done(sXML, null);
@ -350,7 +350,7 @@ function embedMachine(sAppName, sAppClass, sVersion, idMachine, sXMLFile, sXSLFi
var doneMachine = function() {
Component.assert(cAsyncMachines > 0);
if (!--cAsyncMachines) {
if (fAsync) web.enablePageEvents(true);
if (fAsync) Web.enablePageEvents(true);
}
};
@ -378,7 +378,7 @@ function embedMachine(sAppName, sAppClass, sVersion, idMachine, sXMLFile, sXSLFi
var aeWarning = (eMachine && Component.getElementsByClass(eMachine, "machine-warning"));
eWarning = (aeWarning && aeWarning[0]) || eMachine;
}
if (eWarning) eWarning.innerHTML = str.escapeHTML(sMessage);
if (eWarning) eWarning.innerHTML = Str.escapeHTML(sMessage);
};
try {
@ -545,7 +545,7 @@ function embedMachine(sAppName, sAppClass, sVersion, idMachine, sXMLFile, sXSLFi
*/
function embedC1P(idMachine, sXMLFile, sXSLFile)
{
if (fAsync) web.enablePageEvents(false);
if (fAsync) Web.enablePageEvents(false);
return embedMachine("C1Pjs", "c1pjs", APPVERSION, idMachine, sXMLFile, sXSLFile);
}
@ -560,7 +560,7 @@ function embedC1P(idMachine, sXMLFile, sXSLFile)
*/
function embedPCx86(idMachine, sXMLFile, sXSLFile, sParms)
{
if (fAsync) web.enablePageEvents(false);
if (fAsync) Web.enablePageEvents(false);
return embedMachine("PCx86", "pcx86", APPVERSION, idMachine, sXMLFile, sXSLFile, sParms);
}
@ -575,7 +575,7 @@ function embedPCx86(idMachine, sXMLFile, sXSLFile, sParms)
*/
function embedPC8080(idMachine, sXMLFile, sXSLFile, sParms)
{
if (fAsync) web.enablePageEvents(false);
if (fAsync) Web.enablePageEvents(false);
return embedMachine("PC8080", "pc8080", APPVERSION, idMachine, sXMLFile, sXSLFile, sParms);
}
@ -590,10 +590,22 @@ function embedPC8080(idMachine, sXMLFile, sXSLFile, sParms)
*/
function embedPDP11(idMachine, sXMLFile, sXSLFile, sParms)
{
if (fAsync) web.enablePageEvents(false);
if (fAsync) Web.enablePageEvents(false);
return embedMachine("PDPjs", "pdp11", APPVERSION, idMachine, sXMLFile, sXSLFile, sParms);
}
/**
* findMachineComponent(idMachine, sType)
*
* @param {string} idMachine
* @param {string} sType
* @return {Component|null}
*/
function findMachineComponent(idMachine, sType)
{
return Component.getComponentByType(sType, idMachine + ".machine");
}
/**
* Prevent the Closure Compiler from renaming functions we want to export, by adding them as global properties.
*/
@ -611,5 +623,7 @@ if (APPNAME == "PDPjs") {
window['embedPDP11'] = embedPDP11;
}
window['enableEvents'] = web.enablePageEvents;
window['sendEvent'] = web.sendPageEvent;
window['findMachineComponent'] = findMachineComponent;
window['enableEvents'] = Web.enablePageEvents;
window['sendEvent'] = Web.sendPageEvent;

View file

@ -32,7 +32,7 @@ var Keys = {
/*
* Keys and/or key combinations that generate common ASCII codes.
*
* NOTE: If you're looking for a general-purpose ASCII code table, see str.ASCII in strlib.js;
* NOTE: If you're looking for a general-purpose ASCII code table, see Str.ASCII in strlib.js;
* if something's missing, that's probably the more appropriate table to add it to.
*
* TODO: The Closure Compiler doesn't inline all references to these values, at least those with
@ -201,6 +201,7 @@ var Keys = {
*/
Keys.KEYCODE.NUM_CR = Keys.KEYCODE.CR + Keys.KEYCODE.ONRIGHT;
/*
* Maps Firefox keyCodes to their more common keyCode counterparts; a number of entries in this table
* are no longer valid (if indeed they ever were), so they've been commented out. It's likely that I

View file

@ -28,18 +28,345 @@
"use strict";
/* global Buffer: false */
var net = {};
if (NODE) {
var sServerRoot;
var fs = require("fs");
var http = require("http");
var path = require("path");
var url = require("url");
}
class Net {
/**
* 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 Net.asPropagate.
*/
static hasParm(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 Net.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
*/
static propagateParms(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 < Net.asPropagate.length; i++) {
var sQuery = Net.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, um, simple 5-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 "archive/" to the corresponding URL at "http://archive.pcjs.org/"
* 3) Use decodeURI() to eliminate escape sequences (like "%20") so that encodeURI() won't re-encode the "%"
* 4) Use encodeURI() to transform all "htmlspecialchars" and reserved characters into the appropriate sequences
* 5) Massage the result with Net.propagateParms(), so that any special parameters are passed along
*
* @param {string} sURL
* @param {Object} req is the web server's (ie, Express) request object, if any
* @param {boolean} [fDebug]
* @return {string} encoded URL
*/
static encodeURL(sURL, req, fDebug)
{
if (sURL) {
sURL = sURL.replace(/\\/g, '/');
if (!fDebug) {
if (sURL.match(/^[^:?]*archive\//)) {
if (sURL.charAt(0) != '/') sURL = path.join(req.path, sURL);
sURL = "http://archive.pcjs.org" + sURL.replace("/archive/", "/");
}
}
/*
* If the incoming URL already contains URI-style escape sequences (eg, "%20" instead of spaces),
* calling decodeURI() first will eliminate them, preventing encodeURI() from converting leading
* "%" into "%25" and corrupting sequences like "%20" by turning them into "%2520".
*/
return Net.propagateParms(encodeURI(decodeURI(sURL)), req);
}
return sURL;
}
/**
* isRemote(sPath)
*
* TODO: Add support for FTP? HTTPS? Anything else?
*
* @param {string} sPath
* @return {boolean} true if sPath is a (supported) remote path, false if not
*/
static isRemote(sPath)
{
return (sPath.indexOf("http:") === 0);
}
/**
* getStat(sURL, done)
*
* @param {string} sURL
* @param {function(Error,Object)} done
*/
static getStat(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)
*
* TODO: Add support for FTP? HTTPS? Anything else?
*
* @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)
*/
static getFile(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
*/
static downloadFile(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);
});
});
}
/**
* getResource(sURL, dataPost, fAsync, done)
*
* Request the specified resource (sURL), and once the request is complete, notify done().
*
* @param {string} sURL
* @param {Object|null} [dataPost] for a POST request (default is a GET request)
* @param {boolean} [fAsync] is true for an asynchronous request
* @param {function(string,string|null,number)} [done]
* @return {Array|null} Array containing [sResource, nErrorCode], or null if no response yet
*/
static getResource(sURL, dataPost, fAsync, done)
{
var nErrorCode = -1, sResource = null, response = null;
if (Net.isRemote(sURL)) {
/*
* TODO: This code is nothing more than a band-aid. It assumes the URL uses "http:"
* (hence the call to getFile(), which only supports HTTP GET operations), it assumes
* the requested data is UTF-8 string data (which is normally the case, because nearly
* all our requests are for JSON files), it doesn't deal with dataPost, it assumes
* that fAsync is true, and it performs very simplistic error code mapping.
*
* But, it gets the job done for what little we actually ask of it, when our machines
* are running in the Node environment.
*/
Net.getFile(sURL, "utf8", function(err, status, data) {
if (done) done(sURL, data, err? status : 0);
});
} else {
if (!Net.sServerRoot) {
Net.sServerRoot = path.join(path.dirname(fs.realpathSync(__filename)), "../../../");
}
var sFile = path.join(Net.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) {
sResource = s;
nErrorCode = 0;
}
if (done) done(sURL, sResource, nErrorCode);
});
} else {
try {
sResource = 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 (done) done(sURL, sResource, nErrorCode);
response = [sResource, nErrorCode];
}
}
return response;
}
}
/*
* The following are (super-secret) commands that can be added to the URL to enable special features.
*
@ -49,342 +376,24 @@ if (NODE) {
* 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
* any commands listed in Net.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.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";
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"];
Net.asPropagate = [Net.GORT_COMMAND, "autostart"];
Net.sServerRoot = null;
/**
* 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, um, simple 5-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 "archive/" to the corresponding URL at "http://archive.pcjs.org/"
* 3) Use decodeURI() to eliminate escape sequences (like "%20") so that encodeURI() won't re-encode the "%"
* 4) Use encodeURI() to transform all "htmlspecialchars" and reserved characters into the appropriate sequences
* 5) Massage the result with net.propagateParms(), so that any special parameters are passed along
*
* @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(/^[^:?]*archive\//)) {
if (sURL.charAt(0) != '/') sURL = path.join(req.path, sURL);
sURL = "http://archive.pcjs.org" + sURL.replace("/archive/", "/");
}
}
/*
* If the incoming URL already contains URI-style escape sequences (eg, "%20" instead of spaces),
* calling decodeURI() first will eliminate them, preventing encodeURI() from converting leading
* "%" into "%25" and corrupting sequences like "%20" by turning them into "%2520".
*/
return net.propagateParms(encodeURI(decodeURI(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);
});
});
};
/**
* getResource(sURL, dataPost, fAsync, done)
*
* Request the specified resource (sURL), and once the request is complete, notify done().
*
* @param {string} sURL
* @param {Object|null} [dataPost] for a POST request (default is a GET request)
* @param {boolean} [fAsync] is true for an asynchronous request
* @param {function(string,string,number)} [done]
* @return {Array|null} Array containing [sResource, nErrorCode], or null if no response yet
*/
net.getResource = function(sURL, dataPost, fAsync, done)
{
var nErrorCode = -1, sResource = null, response = null;
if (net.isRemote(sURL)) {
/*
* TODO: This code is nothing more than a band-aid. It assumes the URL uses "http:"
* (hence the call to getFile(), which only supports HTTP GET operations), it assumes
* the requested data is UTF-8 string data (which is normally the case, because nearly
* all our requests are for JSON files), it doesn't deal with dataPost, it assumes
* that fAsync is true, and it performs very simplistic error code mapping.
*
* But, it gets the job done for what little we actually ask of it, when our machines
* are running in the Node environment.
*/
Net.getFile(sURL, "utf8", function(err, status, data) {
if (done) done(sURL, data, err? status : 0);
});
} 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) {
sResource = s;
nErrorCode = 0;
}
if (done) done(sURL, sResource, nErrorCode);
});
} else {
try {
sResource = 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 (done) done(sURL, sResource, nErrorCode);
response = [sResource, nErrorCode];
}
}
return response;
};
if (NODE) module.exports = net;
if (NODE) module.exports = Net;

View file

@ -28,8 +28,6 @@
"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.

View file

@ -28,8 +28,6 @@
"use strict";
/* global PRIVATE: true */
/*
* PRIVATE enables certain private features that we don't want enabled on the public web server, so by default,
* PRIVATE is false. For Jekyll configurations, if site.pcjs.private is set, this file gets included, overriding

View file

@ -28,59 +28,60 @@
"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]];
class 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:{}}}
*/
static getArgs()
{
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);
}
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;
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};
}
return {argc: argc, argv: argv};
};
}
if (NODE) module.exports = proc;
if (NODE) module.exports = Proc;

View file

@ -28,12 +28,10 @@
"use strict";
/* global window: true, APPVERSION: false, XMLVERSION: true, DEBUG: true */
if (NODE) {
var Component = require("./component");
var str = require("./strlib");
var web = require("./weblib");
var Str = require("../../shared/lib/strlib");
var Web = require("../../shared/lib/weblib");
var Component = require("../../shared/lib/component");
}
/**
@ -59,12 +57,12 @@ function savePC(idMachine, sPCJSFile, callback)
}
}
if (callback && callback({ state: sState, parms: sParms })) return true;
web.getResource(sPCJSFile, null, true, function(sURL, sResponse, nErrorCode) {
downloadCSS(sURL, sResponse, nErrorCode, [idMachine, str.getBaseName(sPCJSFile, true), sParms, sState]);
Web.getResource(sPCJSFile, null, true, function(sURL, sResponse, nErrorCode) {
downloadCSS(sURL, sResponse, nErrorCode, [idMachine, Str.getBaseName(sPCJSFile, true), sParms, sState]);
});
return true;
}
web.alertUser("Unable to identify machine '" + idMachine + "'");
Component.alertUser("Unable to identify machine '" + idMachine + "'");
return false;
}
@ -83,7 +81,7 @@ function downloadCSS(sURL, sPCJS, nErrorCode, aMachineInfo)
var res = Component.getMachineResources(aMachineInfo[0]);
var sCSSFile = null;
for (var sName in res) {
if (str.endsWith(sName, "components.xsl")) {
if (Str.endsWith(sName, "components.xsl")) {
sCSSFile = sName.replace(".xsl", ".css");
break;
}
@ -94,13 +92,13 @@ function downloadCSS(sURL, sPCJS, nErrorCode, aMachineInfo)
*/
downloadPC(sURL, null, 0, aMachineInfo);
} else {
web.getResource(sCSSFile, null, true, function(sURL, sResponse, nErrorCode) {
Web.getResource(sCSSFile, null, true, function(sURL, sResponse, nErrorCode) {
downloadPC(sURL, sResponse, nErrorCode, aMachineInfo);
});
}
return;
}
web.alertUser("Error (" + nErrorCode + ") requesting " + sURL);
Component.alertUser("Error (" + nErrorCode + ") requesting " + sURL);
}
/**
@ -152,7 +150,7 @@ function downloadPC(sURL, sCSS, nErrorCode, aMachineInfo)
var resOld = Component.getMachineResources(idMachine), resNew = {};
for (var sName in resOld) {
var data = resOld[sName];
var sExt = str.getExtension(sName);
var sExt = Str.getExtension(sName);
if (sExt == "xml") {
/*
* Look through this resource for <disk> entries whose paths do not appear as one of the
@ -169,10 +167,10 @@ function downloadPC(sURL, sCSS, nErrorCode, aMachineInfo)
}
}
}
sXMLFile = sName = str.getBaseName(sName);
sXMLFile = sName = Str.getBaseName(sName);
}
else if (sExt == "xsl") {
sXSLFile = sName = str.getBaseName(sName);
sXSLFile = sName = Str.getBaseName(sName);
}
Component.log("saving resource: '" + sName + "' (" + data.length + " bytes)");
resNew[sName] = data;
@ -202,7 +200,7 @@ function downloadPC(sURL, sCSS, nErrorCode, aMachineInfo)
sPCJS = sPCJS.replace(/\u00A9/g, "&#xA9;");
var sAlert = web.downloadFile(sPCJS, "javascript", false, sScript);
var sAlert = Web.downloadFile(sPCJS, "javascript", false, sScript);
sAlert += ', copy it to your web server as "' + sScript + '", and then add the following to your web page:\n\n';
sAlert += '<div id="' + idMachine + '"></div>\n';
@ -210,10 +208,10 @@ function downloadPC(sURL, sCSS, nErrorCode, aMachineInfo)
sAlert += '<script type="text/javascript" src="' + sScript + '"></script>\n';
sAlert += '<script type="text/javascript">embedPC("' + idMachine + '","' + sXMLFile + '","' + sXSLFile + '");</script>\n\n';
sAlert += 'The machine should appear where the <div> is located.';
web.alertUser(sAlert);
Component.alertUser(sAlert);
return;
}
web.alertUser("Missing XML/XSL resources");
Component.alertUser("Missing XML/XSL resources");
}
/**

View file

@ -29,183 +29,46 @@
"use strict";
if (NODE) {
var web = require("./../../shared/lib/weblib");
var Component = require("./../../shared/lib/component");
var Web = require("../../shared/lib/weblib");
var Component = require("../../shared/lib/component");
}
/**
* State(component, sVersion, sSuffix)
*
* State objects are used by components to save/restore their state.
*
* During a save operation, components add data to a State object via set(), and then return
* the resulting data using data().
*
* During a restore operation, the Computer component passes the results of each data() call
* back to the originating component.
*
* WARNING: Since State objects are low-level objects that have no UI requirements, they do not
* inherit from the Component class, so you should only use class methods of Component, such as
* Component.assert() (or Debugger methods if the Debugger is available).
*
* NOTE: 1.01 is the first version to provide limited save/restore support using localStorage.
* From that point on, care must be taken to insure that any new version that's incompatible with
* previous localStorage data be released with a version number that is at least 1 greater,
* since we're tagging the localStorage data with the integer portion of the version string.
*
* @constructor
* @param {Component} component
* @param {string} [sVersion] is used to append a major version number to the key
* @param {string} [sSuffix] is used to append any additional suffixes to the key
*/
function State(component, sVersion, sSuffix) {
this.id = component.id;
this.key = State.key(component, sVersion, sSuffix);
this.dbg = component.dbg;
this.unload(component.parms);
}
/**
* State.key(component, sVersion, sSuffix)
*
* This encapsulates the key generation code.
*
* @param {Component} component
* @param {string} [sVersion] is used to append a major version number to the key
* @param {string} [sSuffix] is used to append any additional suffixes to the key
* @return {string} key
*/
State.key = function(component, sVersion, sSuffix) {
var key = component.id;
if (sVersion) {
var i = sVersion.indexOf('.');
if (i > 0) key += ".v" + sVersion.substr(0, i);
class State {
/**
* State(component, sVersion, sSuffix)
*
* State objects are used by components to save/restore their state.
*
* During a save operation, components add data to a State object via set(), and then return
* the resulting data using data().
*
* During a restore operation, the Computer component passes the results of each data() call
* back to the originating component.
*
* WARNING: Since State objects are low-level objects that have no UI requirements, they do not
* inherit from the Component class, so you should only use class methods of Component, such as
* Component.assert() (or Debugger methods if the Debugger is available).
*
* NOTE: 1.01 is the first version to provide limited save/restore support using localStorage.
* From that point on, care must be taken to insure that any new version that's incompatible with
* previous localStorage data be released with a version number that is at least 1 greater,
* since we're tagging the localStorage data with the integer portion of the version string.
*
* @param {Component} component
* @param {string} [sVersion] is used to append a major version number to the key
* @param {string} [sSuffix] is used to append any additional suffixes to the key
*/
constructor(component, sVersion, sSuffix)
{
this.id = component.id;
this.dbg = component.dbg;
this.json = "";
this.state = {};
this.fLoaded = this.fParsed = false;
this.key = State.key(component, sVersion, sSuffix);
this.unload(component.parms);
}
if (sSuffix) {
key += "." + sSuffix;
}
return key;
};
/**
* State.compress(aSrc)
*
* @param {Array.<number>|null} aSrc
* @return {Array.<number>|null} is either the original array (aSrc), or a smaller array of "count, value" pairs (aComp)
*/
State.compress = function(aSrc) {
if (aSrc) {
var iSrc = 0;
var iComp = 0;
var aComp = [];
while (iSrc < aSrc.length) {
var n = aSrc[iSrc];
Component.assert(n !== undefined);
var iCompare = iSrc + 1;
while (iCompare < aSrc.length && aSrc[iCompare] === n) iCompare++;
aComp[iComp++] = iCompare - iSrc;
aComp[iComp++] = n;
iSrc = iCompare;
}
if (aComp.length < aSrc.length) return aComp;
}
return aSrc;
};
/**
* State.decompress(aComp)
*
* @param {Array.<number>} aComp
* @param {number} nLength is expected length of decompressed data
* @return {Array.<number>}
*/
State.decompress = function(aComp, nLength) {
var iDst = 0;
var aDst = new Array(nLength);
var iComp = 0;
while (iComp < aComp.length - 1) {
var c = aComp[iComp++];
var n = aComp[iComp++];
while (c--) {
aDst[iDst++] = n;
}
}
Component.assert(aDst.length == nLength);
return aDst;
};
/**
* State.compressEvenOdd(aSrc)
*
* This is a very simple variation on compress() that compresses all the EVEN elements of aSrc first,
* followed by all the ODD elements. This tends to work better on EGA video memory, because when odd/even
* addressing is enabled (eg, for text modes), the DWORD values tend to alternate, which is the worst case
* for compress(), but the best case for compressEvenOdd().
*
* One wrinkle we support: if the first element is uninitialized, then we assume the entire array is undefined,
* and return an empty compressed array. Conversely, decompressEvenOdd() will take an empty compressed array
* and return an uninitialized array.
*
* @param {Array.<number>|null} aSrc
* @return {Array.<number>|null} is either the original array (aSrc), or a smaller array of "count, value" pairs (aComp)
*/
State.compressEvenOdd = function(aSrc) {
if (aSrc) {
var iComp = 0, aComp = [];
if (aSrc[0] !== undefined) {
for (var off = 0; off < 2; off++) {
var iSrc = off;
while (iSrc < aSrc.length) {
var n = aSrc[iSrc];
var iCompare = iSrc + 2;
while (iCompare < aSrc.length && aSrc[iCompare] === n) iCompare += 2;
aComp[iComp++] = (iCompare - iSrc) >> 1;
aComp[iComp++] = n;
iSrc = iCompare;
}
}
}
if (aComp.length < aSrc.length) return aComp;
}
return aSrc;
};
/**
* State.decompressEvenOdd(aComp, nLength)
*
* This is the counterpart to compressEvenOdd(). Note that because there's nothing in the compressed sequence
* that differentiates a compress() sequence from a compressEvenOdd() sequence, you simply have to be consistent:
* if you used even/odd compression, then you must use even/odd decompression.
*
* @param {Array.<number>} aComp
* @param {number} nLength is expected length of decompressed data
* @return {Array.<number>}
*/
State.decompressEvenOdd = function(aComp, nLength) {
var iDst = 0;
var aDst = new Array(nLength);
var iComp = 0;
while (iComp < aComp.length - 1) {
var c = aComp[iComp++];
var n = aComp[iComp++];
while (c--) {
aDst[iDst] = n;
iDst += 2;
}
/*
* The output of a "count,value" pair will never exceed the end of the output array, so as soon as we reach it
* the first time, we know it's time to switch to ODD elements, and as soon as we reach it again, we should be
* done.
*/
Component.assert(iDst <= nLength || iComp == aComp.length);
if (iDst == nLength) iDst = 1;
}
Component.assert(aDst.length == nLength);
return aDst;
};
State.prototype = {
constructor: State,
/**
* set(id, data)
*
@ -213,13 +76,15 @@ State.prototype = {
* @param {number|string} id
* @param {Object|string} data
*/
set: function(id, data) {
set(id, data)
{
try {
this[this.id][id] = data;
this.state[id] = data;
} catch(e) {
Component.log(e.message);
}
},
}
/**
* get(id)
*
@ -227,43 +92,38 @@ State.prototype = {
* @param {number|string} id
* @return {Object|string|null}
*/
get: function(id) {
return this[this.id][id] || null;
},
/**
* value()
*
* Use this instead of data() if you haven't called parse() yet.
*
* @this {State}
* @return {string}
*/
value: function() {
return this[this.id];
},
get(id)
{
return this.state[id] || null;
}
/**
* data()
*
* @this {State}
* @return {Object}
*/
data: function() {
return this[this.id];
},
data()
{
return this.state;
}
/**
* load(s)
* load(json)
*
* WARNING: Make sure you follow this call with either a call to parse() or unload(),
* because any stringified data that we've loaded isn't usable until it's been parsed.
*
* @this {State}
* @param {Object|string|null} [s]
* @param {string|null} [json]
* @return {boolean} true if state exists in localStorage, false if not
*/
load: function(s) {
if (s) {
this[this.id] = s;
load(json)
{
if (json) {
this.json = json;
this.fLoaded = true;
this.fParsed = false;
return true;
}
if (this.fLoaded) {
@ -272,17 +132,18 @@ State.prototype = {
*/
return true;
}
if (web.hasLocalStorage()) {
s = web.getLocalStorageItem(this.key);
if (Web.hasLocalStorage()) {
var s = Web.getLocalStorageItem(this.key);
if (s) {
this[this.id] = s;
this.json = s;
this.fLoaded = true;
if (DEBUG) Component.log("localStorage(" + this.key + "): " + s.length + " bytes loaded");
return true;
}
}
return false;
},
}
/**
* parse()
*
@ -293,27 +154,33 @@ State.prototype = {
* @this {State}
* @return {boolean} true if successful, false if error
*/
parse: function() {
parse()
{
var fSuccess = true;
try {
this[this.id] = JSON.parse(this[this.id]);
} catch (e) {
Component.error(e.message || e);
fSuccess = false;
if (!this.fParsed) {
try {
this.state = JSON.parse(this.json);
this.fParsed = true;
} catch (e) {
Component.error(e.message || e);
fSuccess = false;
}
}
return fSuccess;
},
}
/**
* store()
*
* @this {State}
* @return {boolean} true if successful, false if error
*/
store: function() {
store()
{
var fSuccess = true;
if (web.hasLocalStorage()) {
var s = JSON.stringify(this[this.id]);
if (web.setLocalStorageItem(this.key, s)) {
if (Web.hasLocalStorage()) {
var s = JSON.stringify(this.state);
if (Web.setLocalStorageItem(this.key, s)) {
if (DEBUG) Component.log("localStorage(" + this.key + "): " + s.length + " bytes stored");
} else {
/*
@ -327,20 +194,19 @@ State.prototype = {
}
}
return fSuccess;
},
}
/**
* toString()
*
* We can't know whether this might be called before parse() or after parse(), so we check.
* If before, then this[this.id] will still be in string form; if after, it will be an Object.
*
* @this {State}
* @return {string} JSON-encoded state
*/
toString: function() {
var value = this[this.id];
return (typeof value == "string"? value : JSON.stringify(value));
},
toString()
{
return this.state? JSON.stringify(this.state) : this.json;
}
/**
* unload(parms)
*
@ -351,11 +217,14 @@ State.prototype = {
* @this {State}
* @param {Object} [parms]
*/
unload: function(parms) {
this[this.id] = {};
unload(parms)
{
this.json = "";
this.state = {};
this.fLoaded = this.fParsed = false;
if (parms) this.set("parms", parms);
this.fLoaded = false;
},
}
/**
* clear(fAll)
*
@ -365,19 +234,164 @@ State.prototype = {
* @this {State}
* @param {boolean} [fAll] true to unconditionally clear ALL localStorage for the current domain
*/
clear: function(fAll) {
clear(fAll)
{
this.unload();
var aKeys = web.getLocalStorageKeys();
var aKeys = Web.getLocalStorageKeys();
for (var i = 0; i < aKeys.length; i++) {
var sKey = aKeys[i];
if (sKey && (fAll || sKey.substr(0, this.key.length) == this.key)) {
web.removeLocalStorageItem(sKey);
Web.removeLocalStorageItem(sKey);
if (DEBUG) Component.log("localStorage(" + sKey + ") removed");
aKeys.splice(i, 1);
i = 0;
}
}
}
};
/**
* State.key(component, sVersion, sSuffix)
*
* This encapsulates the key generation code.
*
* @param {Component} component
* @param {string} [sVersion] is used to append a major version number to the key
* @param {string} [sSuffix] is used to append any additional suffixes to the key
* @return {string} key
*/
static key(component, sVersion, sSuffix)
{
var key = component.id;
if (sVersion) {
var i = sVersion.indexOf('.');
if (i > 0) key += ".v" + sVersion.substr(0, i);
}
if (sSuffix) {
key += "." + sSuffix;
}
return key;
}
/**
* State.compress(aSrc)
*
* @param {Array.<number>|null} aSrc
* @return {Array.<number>|null} is either the original array (aSrc), or a smaller array of "count, value" pairs (aComp)
*/
static compress(aSrc)
{
if (aSrc) {
var iSrc = 0;
var iComp = 0;
var aComp = [];
while (iSrc < aSrc.length) {
var n = aSrc[iSrc];
Component.assert(n !== undefined);
var iCompare = iSrc + 1;
while (iCompare < aSrc.length && aSrc[iCompare] === n) iCompare++;
aComp[iComp++] = iCompare - iSrc;
aComp[iComp++] = n;
iSrc = iCompare;
}
if (aComp.length < aSrc.length) return aComp;
}
return aSrc;
}
/**
* State.decompress(aComp)
*
* @param {Array.<number>} aComp
* @param {number} nLength is expected length of decompressed data
* @return {Array.<number>}
*/
static decompress(aComp, nLength)
{
var iDst = 0;
var aDst = new Array(nLength);
var iComp = 0;
while (iComp < aComp.length - 1) {
var c = aComp[iComp++];
var n = aComp[iComp++];
while (c--) {
aDst[iDst++] = n;
}
}
Component.assert(aDst.length == nLength);
return aDst;
}
/**
* State.compressEvenOdd(aSrc)
*
* This is a very simple variation on compress() that compresses all the EVEN elements of aSrc first,
* followed by all the ODD elements. This tends to work better on EGA video memory, because when odd/even
* addressing is enabled (eg, for text modes), the DWORD values tend to alternate, which is the worst case
* for compress(), but the best case for compressEvenOdd().
*
* One wrinkle we support: if the first element is uninitialized, then we assume the entire array is undefined,
* and return an empty compressed array. Conversely, decompressEvenOdd() will take an empty compressed array
* and return an uninitialized array.
*
* @param {Array.<number>|null} aSrc
* @return {Array.<number>|null} is either the original array (aSrc), or a smaller array of "count, value" pairs (aComp)
*/
static compressEvenOdd(aSrc)
{
if (aSrc) {
var iComp = 0, aComp = [];
if (aSrc[0] !== undefined) {
for (var off = 0; off < 2; off++) {
var iSrc = off;
while (iSrc < aSrc.length) {
var n = aSrc[iSrc];
var iCompare = iSrc + 2;
while (iCompare < aSrc.length && aSrc[iCompare] === n) iCompare += 2;
aComp[iComp++] = (iCompare - iSrc) >> 1;
aComp[iComp++] = n;
iSrc = iCompare;
}
}
}
if (aComp.length < aSrc.length) return aComp;
}
return aSrc;
}
/**
* State.decompressEvenOdd(aComp, nLength)
*
* This is the counterpart to compressEvenOdd(). Note that because there's nothing in the compressed sequence
* that differentiates a compress() sequence from a compressEvenOdd() sequence, you simply have to be consistent:
* if you used even/odd compression, then you must use even/odd decompression.
*
* @param {Array.<number>} aComp
* @param {number} nLength is expected length of decompressed data
* @return {Array.<number>}
*/
static decompressEvenOdd(aComp, nLength)
{
var iDst = 0;
var aDst = new Array(nLength);
var iComp = 0;
while (iComp < aComp.length - 1) {
var c = aComp[iComp++];
var n = aComp[iComp++];
while (c--) {
aDst[iDst] = n;
iDst += 2;
}
/*
* The output of a "count,value" pair will never exceed the end of the output array, so as soon as we reach it
* the first time, we know it's time to switch to ODD elements, and as soon as we reach it again, we should be
* done.
*/
Component.assert(iDst <= nLength || iComp == aComp.length);
if (iDst == nLength) iDst = 1;
}
Component.assert(aDst.length == nLength);
return aDst;
}
}
if (NODE) module.exports = State;

File diff suppressed because it is too large Load diff

View file

@ -33,7 +33,6 @@
*
* web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.VERIFY + '&' + UserAPI.QUERY.USER + '=' + sUser;
*/
var UserAPI = {
ENDPOINT: "/api/v1/user",
QUERY: {

View file

@ -28,181 +28,6 @@
"use strict";
var usr = {};
/**
* binarySearch(a, v, fnCompare)
*
* @param {Array} a is an array
* @param {number|string|Array|Object} v
* @param {function((number|string|Array|Object), (number|string|Array|Object))} [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|Array|Object), (number|string|Array|Object))} [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 = Date.now || function() { return +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;
};
/**
* @typedef {{
* mask: number,
@ -216,100 +41,279 @@ var BitField;
*/
var BitFields;
/**
* defineBitFields(bfs)
*
* Prepares a bit field definition for use with getBitField() and setBitField(); eg:
*
* var bfs = usr.defineBitFields({num:20, count:8, btmod:1, type:3});
*
* The above defines a set of bit fields containg four fields: num (bits 0-19), count (bits 20-27), btmod (bit 28), and type (bits 29-31).
*
* usr.setBitField(bfs.num, n, 1);
*
* The above set bit field "bfs.num" in numeric variable "n" to the value 1.
*
* @param {Object} bfs
* @return {BitFields}
*/
usr.defineBitFields = function(bfs)
{
var bit = 0;
for (var f in bfs) {
var width = bfs[f];
var mask = ((1 << width) - 1) << bit;
bfs[f] = {mask: mask, shift: bit};
bit += width;
class Usr {
/**
* binarySearch(a, v, fnCompare)
*
* @param {Array} a is an array
* @param {number|string|Array|Object} v
* @param {function((number|string|Array|Object), (number|string|Array|Object))} [fnCompare]
* @return {number} the index of matching entry if non-negative, otherwise the index of the insertion point
*/
static binarySearch(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;
}
// Component.assert(bit <= 32);
return bfs;
};
/**
* initBitFields(bfs, ...)
*
* @param {BitFields} bfs
* @param {...number} var_args
* @return {number} a value containing all supplied bit fields
*/
usr.initBitFields = function(bfs, var_args)
{
var v = 0, i = 1;
for (var f in bfs) {
if (i >= arguments.length) break;
v = usr.setBitField(bfs[f], v, arguments[i++]);
/**
* 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|Array|Object), (number|string|Array|Object))} [fnCompare]
*/
static binaryInsert(a, v, fnCompare)
{
var index = Usr.binarySearch(a, v, fnCompare);
if (index < 0) {
a.splice(-(index + 1), 0, v);
}
}
return v;
};
/**
* getBitField(bf, v)
*
* @param {BitField} bf
* @param {number} v is a value containing bit fields
* @return {number} the value of the bit field in v defined by bf
*/
usr.getBitField = function(bf, v)
{
return (v & bf.mask) >> bf.shift;
};
/**
* setBitField(bf, v, n)
*
* @param {BitField} bf
* @param {number} v is a value containing bit fields
* @param {number} n is a value to store in v in the bit field defined by bf
* @return {number} updated v
*/
usr.setBitField = function(bf, v, n)
{
// Component.assert(!(n & ~(bf.mask >>> bf.shift)));
return (v & ~bf.mask) | ((n << bf.shift) & bf.mask);
};
/**
* indexOf(a, t, i)
*
* Use this instead of Array.prototype.indexOf() if you can't be sure the browser supports it.
*
* @param {Array} a
* @param {*} t
* @param {number} [i]
* @returns {number}
*/
usr.indexOf = function(a, t, i)
{
if (Array.prototype.indexOf) {
return a.indexOf(t, i);
/**
* getTimestamp()
*
* @return {string} timestamp containing the current date and time ("yyyy-mm-dd hh:mm:ss")
*/
static getTimestamp()
{
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());
}
i = i || 0;
if (i < 0) i += a.length;
if (i < 0) i = 0;
for (var n = a.length; i < n; i++) {
if (i in a && a[i] === t) return i;
}
return -1;
};
if (NODE) module.exports = usr;
/**
* 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
*/
static getMonthDays(nMonth, nYear)
{
var nDays = Usr.aMonthDays[nMonth - 1];
if (nDays == 28) {
if ((nYear % 4) === 0 && ((nYear % 100) || (nYear % 400) === 0)) {
nDays++;
}
}
return nDays;
}
/**
* 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
*/
static formatDate(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;
}
/**
* defineBitFields(bfs)
*
* Prepares a bit field definition for use with getBitField() and setBitField(); eg:
*
* var bfs = Usr.defineBitFields({num:20, count:8, btmod:1, type:3});
*
* The above defines a set of bit fields containing four fields: num (bits 0-19), count (bits 20-27), btmod (bit 28), and type (bits 29-31).
*
* Usr.setBitField(bfs.num, n, 1);
*
* The above set bit field "bfs.num" in numeric variable "n" to the value 1.
*
* @param {Object} bfs
* @return {BitFields}
*/
static defineBitFields(bfs)
{
var bit = 0;
for (var f in bfs) {
var width = bfs[f];
var mask = ((1 << width) - 1) << bit;
bfs[f] = {mask: mask, shift: bit};
bit += width;
}
return bfs;
}
/**
* initBitFields(bfs, ...)
*
* @param {BitFields} bfs
* @param {...number} var_args
* @return {number} a value containing all supplied bit fields
*/
static initBitFields(bfs, var_args)
{
var v = 0, i = 1;
for (var f in bfs) {
if (i >= arguments.length) break;
v = Usr.setBitField(bfs[f], v, arguments[i++]);
}
return v;
}
/**
* getBitField(bf, v)
*
* @param {BitField} bf
* @param {number} v is a value containing bit fields
* @return {number} the value of the bit field in v defined by bf
*/
static getBitField(bf, v)
{
return (v & bf.mask) >> bf.shift;
}
/**
* setBitField(bf, v, n)
*
* @param {BitField} bf
* @param {number} v is a value containing bit fields
* @param {number} n is a value to store in v in the bit field defined by bf
* @return {number} updated v
*/
static setBitField(bf, v, n)
{
return (v & ~bf.mask) | ((n << bf.shift) & bf.mask);
}
/**
* indexOf(a, t, i)
*
* Use this instead of Array.prototype.indexOf() if you can't be sure the browser supports it.
*
* @param {Array} a
* @param {*} t
* @param {number} [i]
* @returns {number}
*/
static indexOf(a, t, i)
{
if (Array.prototype.indexOf) {
return a.indexOf(t, i);
}
i = i || 0;
if (i < 0) i += a.length;
if (i < 0) i = 0;
for (var n = a.length; i < n; i++) {
if (i in a && a[i] === t) return i;
}
return -1;
}
}
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];
/**
* getTime()
*
* @return {number} the current time, in milliseconds
*/
Usr.getTime = Date.now || function() { return +new Date(); };
if (NODE) module.exports = Usr;

File diff suppressed because it is too large Load diff