Merge branch 'next-release'

# Conflicts:
#	versions/pc8080/1.24.0/pc8080-dbg.js
#	versions/pc8080/1.24.0/pc8080.js
#	versions/pcx86/1.23.0/components.xsl
#	versions/pcx86/1.23.0/pcx86-dbg.js
#	versions/pcx86/1.23.0/pcx86.js
#	versions/pcx86/1.24.0/pcx86-dbg.js
#	versions/pcx86/1.24.0/pcx86.js
This commit is contained in:
Jeff Parsons 2016-12-18 13:44:47 -08:00 committed by Jeff Parsons
commit 5fca80331a
999 changed files with 22351 additions and 211343 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,677 @@
/**
* @fileoverview Common PCjs Debugger support.
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @copyright © Jeff Parsons 2012-2016
*
* This file is part of PCjs, a computer emulation software project at <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 modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <http://pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"use strict";
import Str from "../../shared/es6/strlib";
import Component from "../..shared/es6/component";
/**
* Debugger Address Object
*
* This is the basic structure; other debuggers may extend it.
*
* addr address
* fTemporary true if this is a temporary breakpoint address
* sCmd set for breakpoint addresses if there's an associated command string
* aCmds preprocessed commands (from sCmd)
*
* @typedef {{
* addr:(number|undefined),
* fTemporary:(boolean|undefined),
* sCmd:(string|undefined),
* aCmds:(Array.<string>|undefined)
* }} DbgAddr
*/
var DbgAddr;
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) {
super("Debugger", parmsDbg, Debugger);
/*
* 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;
/*
* 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 = [];
/*
* 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
}
/**
* getRegIndex(sReg, off)
*
* NOTE: This must be implemented by the individual debuggers.
*
* @this {Debugger}
* @param {string} sReg
* @param {number} [off] optional offset into sReg
* @return {number} register index, or -1 if not found
*/
getRegIndex(sReg, off) {
return -1;
}
/**
* getRegValue(iReg)
*
* NOTE: This must be implemented by the individual debuggers.
*
* @this {Debugger}
* @param {number} iReg
* @return {number|undefined}
*/
getRegValue(iReg) {
return undefined;
}
/**
* parseAddrReference(s, sAddr)
*
* Returns the given string with the given address reference replaced with the contents of that address.
*
* NOTE: This must be implemented by the individual debuggers.
*
* @this {Debugger}
* @param {string} s
* @param {string} sAddr
* @return {string}
*/
parseAddrReference(s, sAddr) {
return s.replace('[' + sAddr + ']', "unimplemented");
}
/**
* getNextCommand()
*
* @this {Debugger}
* @return {string}
*/
getNextCommand() {
var sCmd;
if (this.iPrevCmd > 0) {
sCmd = this.aPrevCmds[--this.iPrevCmd];
} else {
sCmd = "";
this.iPrevCmd = -1;
}
return sCmd;
}
/**
* getPrevCommand()
*
* @this {Debugger}
* @return {string|null}
*/
getPrevCommand() {
var sCmd = null;
if (this.iPrevCmd < this.aPrevCmds.length - 1) {
sCmd = this.aPrevCmds[++this.iPrevCmd];
}
return sCmd;
}
/**
* parseCommand(sCmd, fSave, chSep)
*
* @this {Debugger}
* @param {string|undefined} sCmd
* @param {boolean} [fSave] is true to save the command, false if not
* @param {string} [chSep] is the command separator character (default is ';')
* @return {Array.<string>}
*/
parseCommand(sCmd, fSave, chSep) {
if (fSave) {
if (!sCmd) {
if (this.fAssemble) {
sCmd = "end";
} else {
sCmd = this.aPrevCmds[this.iPrevCmd+1];
}
} else {
if (this.iPrevCmd < 0 && this.aPrevCmds.length) {
this.iPrevCmd = 0;
}
if (this.iPrevCmd < 0 || sCmd != this.aPrevCmds[this.iPrevCmd]) {
this.aPrevCmds.splice(0, 0, sCmd);
this.iPrevCmd = 0;
}
this.iPrevCmd--;
}
}
var a = [];
if (sCmd) {
/*
* With the introduction of breakpoint commands (ie, quoted command sequences
* 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]);
*
* We may now split on semi-colons ONLY if they are outside a quoted sequence.
*
* Also, to allow quoted strings *inside* breakpoint commands, we first replace all
* DOUBLE double-quotes with single quotes.
*/
sCmd = sCmd.replace(/""/g, "'");
var iPrev = 0;
var chQuote = null;
chSep = chSep || ';';
/*
* NOTE: Processing charAt() up to and INCLUDING length is not a typo; we're taking
* advantage of the fact that charAt() with an invalid index returns an empty string,
* allowing us to use the same substring() call to capture the final portion of sCmd.
*
* In a sense, it allows us to pretend that the string ends with a zero terminator.
*/
for (var i = 0; i <= sCmd.length; i++) {
var ch = sCmd.charAt(i);
if (ch == '"' || ch == "'") {
if (!chQuote) {
chQuote = ch;
} else if (ch == chQuote) {
chQuote = null;
}
}
else if (ch == chSep && !chQuote || !ch) {
/*
* 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)));
iPrev = i + 1;
}
}
}
return a;
}
/**
* evalExpression(aVals, aOps, cOps)
*
* In Node, if you set a variable to 0x80000001; ie:
*
* foo=0x80000001|0
*
* and then calculate foo*foo using "(foo*foo).toString(2)", the result is:
*
* '11111111111111111111111111111100000000000000000000000000000000'
*
* which is slightly incorrect because it has overflowed JavaScript's floating-point precision.
*
* 0x80000001 in decimal is -2147483647, so the product is 4611686014132420609, which is 0x3FFFFFFF00000001.
*
* @this {Debugger}
* @param {Array.<number>} aVals
* @param {Array.<string>} aOps
* @param {number} [cOps] (default is all)
* @return {boolean} true if successful, false if error
*/
evalExpression(aVals, aOps, cOps) {
cOps = cOps || -1;
while (cOps-- && aOps.length) {
var chOp = aOps.pop();
if (aVals.length < 2) return false;
var valNew;
var val2 = aVals.pop();
var val1 = aVals.pop();
switch(chOp) {
case '*':
valNew = val1 * val2;
break;
case '/':
if (!val2) return false;
valNew = val1 / val2;
break;
case '%':
if (!val2) return false;
valNew = val1 % val2;
break;
case '+':
valNew = val1 + val2;
break;
case '-':
valNew = val1 - val2;
break;
case '<<':
valNew = val1 << val2;
break;
case '>>':
valNew = val1 >> val2;
break;
case '>>>':
valNew = val1 >>> val2;
break;
case '<':
valNew = (val1 < val2? 1 : 0);
break;
case '<=':
valNew = (val1 <= val2? 1 : 0);
break;
case '>':
valNew = (val1 > val2? 1 : 0);
break;
case '>=':
valNew = (val1 >= val2? 1 : 0);
break;
case '==':
valNew = (val1 == val2? 1 : 0);
break;
case '!=':
valNew = (val1 != val2? 1 : 0);
break;
case '&':
valNew = val1 & val2;
break;
case '^':
valNew = val1 ^ val2;
break;
case '|':
valNew = val1 | val2;
break;
case '&&':
valNew = (val1 && val2? 1 : 0);
break;
case '||':
valNew = (val1 || val2? 1 : 0);
break;
default:
return false;
}
aVals.push(valNew|0);
}
return true;
}
/**
* parseExpression(sExp, fPrint)
*
* A quick-and-dirty expression parser. It takes an expression like:
*
* EDX+EDX*4+12345678
*
* and builds a value stack in aVals and a "binop" (binary operator) stack in aOps:
*
* aVals aOps
* ----- ----
* EDX +
* EDX *
* 4 +
* ...
*
* We pop 1 "binop" from aOps and 2 values from aVals whenever a "binop" of lower priority than its
* predecessor is encountered, evaluate, and push the result back onto aVals.
*
* Unary operators like '~' and ternary operators like '?:' are not supported; neither are parentheses.
*
* However, parseReference() now makes it possible to write parenthetical-style sub-expressions by using
* {...} (braces), as well as address references by using [...] (brackets).
*
* Why am I using braces instead of parentheses for sub-expressions? Because parseReference() serves
* multiple purposes, the other being reference replacement in message strings passing through replaceRegs(),
* and I didn't want parentheses taking on a new meaning in message strings.
*
* However, a Debugger can override this choice by setting fParens to true, if there's no conflict in its
* replaceRegs() implementation.
*
* @this {Debugger}
* @param {string|undefined} sExp
* @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
*/
parseExpression(sExp, fPrint) {
var value;
if (sExp) {
/*
* First process (and eliminate) any references, aka sub-expressions.
*/
sExp = this.parseReference(sExp);
var i = 0;
var fError = false;
var sExpOrig = sExp;
var aVals = [], aOps = [];
/*
* All browsers (including, I believe, IE9 and up) support the following idiosyncrasy of a regexp split():
* when the regexp uses a capturing pattern, the resulting array will include entries for all the pattern
* matches along with the non-matches. This effectively means that, in the set of expressions that we
* support, all even entries in asValues will contain "values" and all odd entries will contain "operators".
*
* And although I tried to list the supported operators in "precedential" order, bitwise operators must
* be out-of-order so that we don't mistakenly match either '>' or '<' when they're part of '>>' or '<<'.
*/
var regExp = /(\|\||&&|\||^|&|!=|==|>=|>>>|>>|>|<=|<<|<|-|\+|%|\/|\*)/;
var asValues = sExp.split(regExp);
while (i < asValues.length) {
var sValue = asValues[i++];
var cchValue = sValue.length;
var s = Str.trim(sValue);
if (!s) {
fError = true;
break;
}
var v = this.parseValue(s, null, fPrint === false);
if (v === undefined) {
fError = true;
fPrint = false;
break;
}
aVals.push(v);
if (i == asValues.length) break;
var sOp = asValues[i++], cchOp = sOp.length;
this.assert(Debugger.aBinOpPrecedence[sOp] != null);
if (aOps.length && Debugger.aBinOpPrecedence[sOp] < Debugger.aBinOpPrecedence[aOps[aOps.length-1]]) {
this.evalExpression(aVals, aOps, 1);
}
aOps.push(sOp);
sExp = sExp.substr(cchValue + cchOp);
}
if (!this.evalExpression(aVals, aOps) || aVals.length != 1) {
fError = true;
}
if (!fError) {
value = aVals.pop();
if (fPrint) this.printValue(null, value);
} else {
if (fPrint) this.println("error parsing '" + sExpOrig + "' at character " + (sExpOrig.length - sExp.length));
}
}
return value;
}
/**
* parseReference(s)
*
* Returns the given string with any "{expression}" sequences replaced with the value of the expression,
* and any "[address]" references replaced with the contents of the address. Expressions are parsed BEFORE
* addresses. Owing to this function's simplistic parsing, nested braces/brackets are not supported
* (define intermediate variables if needed).
*
* @this {Debugger}
* @param {string} s
* @return {string}
*/
parseReference(s) {
var a;
var chOpen = this.fParens? '(' : '{';
var chClose = this.fParens? ')' : '}';
var reSubExp = new RegExp(this.fParens? "\\((.*?)\\)" : "\\{(.*?)\\}");
while (a = s.match(reSubExp)) {
if (a[1].indexOf(chOpen) >= 0) break; // unsupported nested brace(s)
var value = this.parseExpression(a[1]);
s = s.replace(chOpen + a[1] + chClose, value != null? this.toStrBase(value) : "undefined");
}
while (a = s.match(/\[(.*?)]/)) {
if (a[1].indexOf('[') >= 0) break; // unsupported nested bracket(s)
s = this.parseAddrReference(s, a[1]);
}
return this.parseSysVars(s);
}
/**
* parseSysVars(s)
*
* Returns the given string with any recognized "$var" replaced with its value; eg:
*
* $ops: the number of opcodes executed since the last time it was displayed (or reset)
*
* @this {Debugger}
* @param {string} s
* @return {string}
*/
parseSysVars(s) {
var a;
while (a = s.match(/\$([a-z]+)/i)) {
var v = null;
switch(a[1].toLowerCase()) {
case "ops":
v = this.cOpcodes - this.cOpcodesStart;
break;
}
if (v == null) break;
s = s.replace(a[0], v.toString());
}
return s;
}
/**
* parseValue(sValue, sName, fQuiet)
*
* @this {Debugger}
* @param {string|undefined} sValue
* @param {string|null} [sName] is the name of the value, if any
* @param {boolean} [fQuiet]
* @return {number|undefined} numeric value, or undefined if sValue is either undefined or invalid
*/
parseValue(sValue, sName, fQuiet) {
var value;
if (sValue != null) {
var iReg = this.getRegIndex(sValue);
if (iReg >= 0) {
value = this.getRegValue(iReg);
} else {
value = this.getVariable(sValue);
if (value == null) {
value = Str.parseInt(sValue, this.nBase);
}
}
if (value == null && !fQuiet) this.println("invalid " + (sName? sName : "value") + ": " + sValue);
} else {
if (!fQuiet) this.println("missing " + (sName || "value"));
}
return value;
}
/**
* printValue(sVar, value)
*
* @this {Debugger}
* @param {string|null} sVar
* @param {number|undefined} value
* @return {boolean} true if value defined, false if not
*/
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);
if (value >= 0x20 && value < 0x7F) {
sValue += " '" + String.fromCharCode(value) + "'";
}
}
sVar = (sVar != null? (sVar + ": ") : "");
this.println(sVar + sValue);
return fDefined;
}
/**
* printVariable(sVar)
*
* @this {Debugger}
* @param {string} [sVar]
* @return {boolean} true if all value(s) defined, false if not
*/
printVariable(sVar) {
if (sVar) {
return this.printValue(sVar, this.aVariables[sVar]);
}
var cVariables = 0;
for (sVar in this.aVariables) {
this.printValue(sVar, this.aVariables[sVar]);
cVariables++;
}
return cVariables > 0;
}
/**
* delVariable(sVar)
*
* @this {Debugger}
* @param {string} sVar
*/
delVariable(sVar) {
delete this.aVariables[sVar];
}
/**
* getVariable(sVar)
*
* @this {Debugger}
* @param {string} sVar
* @return {number|undefined}
*/
getVariable(sVar) {
return this.aVariables[sVar];
}
/**
* setVariable(sVar, value)
*
* @this {Debugger}
* @param {string} sVar
* @param {number} 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.
*
* @this {Debugger}
* @param {number|null|undefined} n
* @param {number} [nBytes] is the number of bytes to display, which we translate into a number of characters
* @param {boolean} [fStripLeadingZeros]
* @return {string}
*/
toStrBase(n, nBytes, fStripLeadingZeros) {
var s;
switch(this.nBase) {
case 8:
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);
break;
}
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
export default Debugger;

View file

@ -0,0 +1,112 @@
/**
* @fileoverview Compile-time definitions used by C1Pjs and PCjs.
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @copyright © Jeff Parsons 2012-2016
*
* This file is part of PCjs, a computer emulation software project at <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 modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <http://pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"use strict";
/**
* @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-2016 Jeff Parsons <Jeff@pcjs.org>";
var LICENSE = "License: GPL version 3 or later <http://gnu.org/licenses/gpl.html>";
var CSSCLASS = "pcjs";
/**
* @define {string}
*/
var SITEHOST = "localhost: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
/**
* @define {boolean}
*/
var PRIVATE = false; // this @define is overridden by the Closure Compiler (to false) to enable PRIVATE code
/*
* RS-232 DB-25 Pin Definitions, mapped to bits 1-25 in a 32-bit status value.
*
* SerialPorts in PCjs machines are considered DTE (Data Terminal Equipment), which means they should be "virtually"
* connected to each other via a null-modem cable, which assumes the following cross-wiring:
*
* G 1 <-> 1 G (Ground)
* TD 2 <-> 3 RD (Received Data)
* RD 3 <-> 2 TD (Transmitted Data)
* RTS 4 <-> 5 CTS (Clear To Send)
* CTS 5 <-> 4 RTS (Request To Send)
* DSR 6+8 <-> 20 DTR (Data Terminal Ready)
* SG 7 <-> 7 SG (Signal Ground)
* DTR 20 <-> 6+8 DSR (Data Set Ready + Carrier Detect)
* RI 22 <-> 22 RI (Ring Indicator)
*
* TODO: Move these definitions to a more appropriate shared file at some point.
*/
var RS232 = {
RTS: {
PIN: 4,
MASK: 0x00000010
},
CTS: {
PIN: 5,
MASK: 0x00000020
},
DSR: {
PIN: 6,
MASK: 0x00000040
},
CD: {
PIN: 8,
MASK: 0x00000100
},
DTR: {
PIN: 20,
MASK: 0x00100000
},
RI: {
PIN: 22,
MASK: 0x00400000
}
};

View file

@ -0,0 +1,242 @@
/**
* @fileoverview Disk APIs, as defined by httpapi.js and consumed by disk.js
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @copyright © Jeff Parsons 2012-2016
*
* This file is part of PCjs, a computer emulation software project at <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 modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <http://pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"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"
}
};
/*
* Common (supported) diskette formats
*
* 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]
*
* 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)
/*
* 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
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
};
/*
* TODO: Eventually, our tools will need to support looking up disk formats by "model" rather than by raw disk size,
* because obviously multiple disk geometries can yield the same raw disk size. For each conflict that arises, I'll
* probably create a fake (approximate) disk size entry above, and then create a mapping to that approximate size below.
*/
DiskAPI.DISK_MODELS = {
"RL01": 5242880,
"RL02": 10485760
};
DiskAPI.MBR = {
PARTITIONS: {
OFFSET: 0x1BE,
ENTRY: {
STATUS: 0x00, // 0x80 if active
CHS_FIRST: 0x01, // 3-byte CHS specifier
TYPE: 0x04, // see TYPE.*
CHS_LAST: 0x05, // 3-byte CHS specifier
LBA_FIRST: 0x08,
LBA_TOTAL: 0x0C,
LENGTH: 0x10
},
STATUS: {
ACTIVE: 0x80
},
TYPE: {
EMPTY: 0x00,
FAT12_PRIMARY: 0x01, // DOS 2.0 and up (12-bit FAT)
FAT16_PRIMARY: 0x04 // DOS 3.0 and up (16-bit FAT)
}
},
SIG_OFFSET: 0x1FE,
SIGNATURE: 0xAA55 // to be clear, the low byte (at offset 0x1FE) is 0x55 and the high byte (at offset 0x1FF) is 0xAA
};
/*
* Boot sector offsets (and assorted constants) in DOS-compatible boot sectors (DOS 2.0 and up)
*
* WARNING: I've heard apocryphal stories about SIGNATURE being improperly reversed on some systems
* (ie, 0x55AA instead 0xAA55) -- perhaps by a dyslexic programmer -- so be careful out there.
*/
DiskAPI.BOOT = {
JMP_OPCODE: 0x000, // 1 byte for a JMP opcode, followed by a 1 or 2-byte offset
OEM_STRING: 0x003, // 8 bytes
SIG_OFFSET: 0x1FE,
SIGNATURE: 0xAA55 // to be clear, the low byte (at offset 0x1FE) is 0x55 and the high byte (at offset 0x1FF) is 0xAA
};
/*
* BIOS Parameter Block (BPB) offsets in DOS-compatible boot sectors (DOS 2.0 and up)
*/
DiskAPI.BPB = {
SECTOR_BYTES: 0x00B, // 2 bytes: bytes per sector (eg, 0x200 or 512)
CLUSTER_SECS: 0x00D, // 1 byte: sectors per cluster (eg, 1)
RESERVED_SECS: 0x00E, // 2 bytes: reserved sectors; ie, # sectors preceding the first FAT--usually just the boot sector (eg, 1)
TOTAL_FATS: 0x010, // 1 byte: FAT copies (eg, 2)
ROOT_DIRENTS: 0x011, // 2 bytes: root directory entries (eg, 0x40 or 64) 0x40 * 0x20 = 0x800 (1 sector is 0x200 bytes, total of 4 sectors)
TOTAL_SECS: 0x013, // 2 bytes: number of sectors (eg, 0x140 or 320); if zero, refer to LARGE_SECS
MEDIA_TYPE: 0x015, // 1 byte: media type (see DiskAPI.FAT.MEDIA_*)
FAT_SECS: 0x016, // 2 bytes: sectors per FAT (eg, 1)
TRACK_SECS: 0x018, // 2 bytes: sectors per track (eg, 8)
TOTAL_HEADS: 0x01A, // 2 bytes: number of heads (eg, 1)
HIDDEN_SECS: 0x01C, // 4 bytes: number of hidden sectors (always 0 for non-partitioned media)
LARGE_SECS: 0x020 // 4 bytes: number of sectors if TOTAL_SECS is zero
};
/*
* Media descriptor bytes for DOS-compatible FAT-formatted disks (stored in the first byte of the FAT)
*/
DiskAPI.FAT = {
MEDIA_160KB: 0xFE, // 5.25-inch, 1-sided, 8-sector, 40-track
MEDIA_180KB: 0xFC, // 5.25-inch, 1-sided, 9-sector, 40-track
MEDIA_320KB: 0xFF, // 5.25-inch, 2-sided, 8-sector, 40-track
MEDIA_360KB: 0xFD, // 5.25-inch, 2-sided, 9-sector, 40-track
MEDIA_720KB: 0xF9, // 3.5-inch, 2-sided, 9-sector, 80-track
MEDIA_1200KB: 0xF9, // 3.5-inch, 2-sided, 15-sector, 80-track
MEDIA_1440KB: 0xF0, // 3.5-inch, 2-sided, 18-sector, 80-track
MEDIA_2880KB: 0xF0 // 3.5-inch, 2-sided, 36-sector, 80-track
};
/*
* Cluster constants for 12-bit FATs (CLUSNUM_FREE, CLUSNUM_RES and CLUSNUM_MIN are the same for all FATs)
*/
DiskAPI.FAT12 = {
MAX_CLUSTERS: 4084,
CLUSNUM_FREE: 0, // this should NEVER appear in cluster chain (except at the start of an empty chain)
CLUSNUM_RES: 1, // reserved; this should NEVER appear in cluster chain
CLUSNUM_MIN: 2, // smallest valid cluster number
CLUSNUM_MAX: 0xFF6, // largest valid cluster number
CLUSNUM_BAD: 0xFF7, // bad cluster; this should NEVER appear in cluster chain
CLUSNUM_EOC: 0xFF8 // end of chain (actually, anything from 0xFF8-0xFFF indicates EOC)
};
/*
* Cluster constants for 16-bit FATs (CLUSNUM_FREE, CLUSNUM_RES and CLUSNUM_MIN are the same for all FATs)
*/
DiskAPI.FAT16 = {
MAX_CLUSTERS: 65524,
CLUSNUM_FREE: 0, // this should NEVER appear in cluster chain (except at the start of an empty chain)
CLUSNUM_RES: 1, // reserved; this should NEVER appear in cluster chain
CLUSNUM_MIN: 2, // smallest valid cluster number
CLUSNUM_MAX: 0xFFF6, // largest valid cluster number
CLUSNUM_BAD: 0xFFF7, // bad cluster; this should NEVER appear in cluster chain
CLUSNUM_EOC: 0xFFF8 // end of chain (actually, anything from 0xFFF8-0xFFFF indicates EOC)
};
/*
* Directory Entry offsets (and assorted constants) in FAT disk images
*
* NOTE: Versions of DOS prior to 2.0 use INVALID exclusively to mark available directory entries; any entry marked
* UNUSED will actually be considered USED. In DOS 2.0 and up, UNUSED was added to indicate that all remaining entries
* are unused, relieving it from having to initialize the rest of the sectors in the directory cluster(s). And in fact,
* you WILL encounter garbage in subsequent directory sectors if you attempt to read past an UNUSED entry.
*/
DiskAPI.DIRENT = {
NAME: 0x000, // 8 bytes
EXT: 0x008, // 3 bytes
ATTR: 0x00B, // 1 byte
MODTIME: 0x016, // 2 bytes
MODDATE: 0x018, // 2 bytes
CLUSTER: 0x01A, // 2 bytes
SIZE: 0x01C, // 4 bytes (typically zero for subdirectories)
LENGTH: 0x20, // 32 bytes total
UNUSED: 0x00, // indicates this and all subsequent directory entries are unused
INVALID: 0xE5 // indicates this directory entry is unused
};
/*
* Possible values for DIRENT.ATTR
*/
DiskAPI.ATTR = {
READONLY: 0x01, // PC-DOS 2.0 and up
HIDDEN: 0x02,
SYSTEM: 0x04,
LABEL: 0x08, // PC-DOS 2.0 and up
SUBDIR: 0x10, // PC-DOS 2.0 and up
ARCHIVE: 0x20 // PC-DOS 2.0 and up
};
export default 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>
* @copyright © Jeff Parsons 2012-2016
*
* This file is part of PCjs, a computer emulation software project at <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 modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <http://pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"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/rom/5150/1981-04-24/PCBIOS-REV1.rom&format=json
*
* and that request now looks like:
*
* http://www.pcjs.org/api/v1/dump?file=/devices/pc/rom/5150/1981-04-24/PCBIOS-REV1.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 drive size in Mb (formerly "mbsize") (DiskDump only) (DEPRECATED)
SIZE: "size" // value is target disk size in Kb (supersedes "mbhd") (DiskDump only)
},
FORMAT: {
JSON: "json", // default
JSON_GZ: "gz", // gzip is currently used ONLY for compressed JSON
DATA: "data", // same as "json", but built without JSON.stringify() (DiskDump only)
HEX: "hex", // deprecated
OCTAL: "octal", // displays data as octal words
BYTES: "bytes", // displays data as hex bytes; normally used only when comments are enabled
WORDS: "words", // displays data as hex words; 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];
export default DumpAPI;

613
modules/shared/es6/embed.js Normal file
View file

@ -0,0 +1,613 @@
/**
* @fileoverview C1Pjs and PCjs embedding functionality.
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @copyright © Jeff Parsons 2012-2016
*
* This file is part of PCjs, a computer emulation software project at <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 modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <http://pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"use strict";
import Str from "../../shared/es6/strlib";
import Web from "../../shared/es6/weblib";
import Component from "../../shared/es6/component";
/*
* We now support asynchronous XML and XSL file loads; simply set fAsync (below) to true.
*
* NOTE: For that support to work, we have to keep track of the number of machines on the page
* (ie, how many embedMachine() calls were issued), reduce the count as each machine XML file
* is fully transformed into HTML, and when the count finally returns to zero, notify all the
* machine component init() handlers.
*
* Also, to prevent those init() handlers from running prematurely, we must disable all page
* notification events at the start of the embedding process (Web.enablePageEvents(false)) and
* re-enable them at the end (Web.enablePageEvents(true)).
*/
var fAsync = true;
var cAsyncMachines = 0;
/**
* loadXML(sFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done)
*
* This is the preferred way to load all XML and XSL files. It uses getResource()
* 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 resolveXML(),
* 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|null|undefined} idMachine
* @param {string|null|undefined} sAppName
* @param {string|null|undefined} sAppClass
* @param {string|null|undefined} sParms
* @param {boolean} fResolve is true to resolve any "ref" attributes
* @param {function(string)} display
* @param {function(string,Object)} done (string contains the unparsed XML string data, and Object contains a parsed XML object)
*/
function loadXML(sXMLFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done)
{
var doneLoadXML = function(sURLName, sXML, nErrorCode) {
if (nErrorCode) {
if (!sXML) sXML = "unable to load " + sXMLFile + " (" + nErrorCode + ")";
done(sXML, null);
return;
}
parseXML(sXML, sXMLFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done);
};
display("Loading " + sXMLFile + "...");
Web.getResource(sXMLFile, null, fAsync, doneLoadXML);
}
/**
* parseXML(sXML, sXMLFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done)
*
* Generates an XML document from an XML string. This function also provides a work-around for XSLT's
* lack of support for the document() function (at least on some browsers), by replacing every reference
* tag (ie, a tag with a "ref" attribute) with the contents of the referenced file.
*
* @param {string} sXML
* @param {string|null} sXMLFile
* @param {string|null|undefined} idMachine
* @param {string|null|undefined} sAppName
* @param {string|null|undefined} sAppClass
* @param {string|null|undefined} sParms
* @param {boolean} fResolve is true to resolve any "ref" attributes; default is false
* @param {function(string)} display
* @param {function(string,Object)} done (string contains the unparsed XML string data, and Object contains a parsed XML object)
*/
function parseXML(sXML, sXMLFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done)
{
var buildXML = function(sXML, sError) {
if (sError) {
done(sError, null);
return;
}
if (idMachine) {
/*
* A more sensible place to record the machine XML would be embedMachine(), like we do for the
* XSL file, but since we're about to modify the original machine XML, it's best to record it now.
*/
Component.addMachineResource(idMachine, sXMLFile, sXML);
var sURL = sXMLFile;
if (sURL && sURL.indexOf('/') < 0 && window.location.pathname.slice(-1) == '/') {
sURL = window.location.pathname + sURL;
}
/*
* We embed the URL of the XML file both as a separate "xml" attribute for easy access from the
* XSL file, and as part of the "parms" attribute for easy access from machines (see getMachineParm()).
*/
if (!sParms) {
sParms = '{';
} else if (sParms.slice(-1) == '}') {
sParms = sParms.slice(0, -1);
if (sParms.length > 1) sParms += ',';
} else { // sParms must just be a "state" file, so encode it as a "state" property
sParms = '{state:"' + sParms + '",';
}
sParms += 'url:"' + sURL + '"}';
/*
* Note that while we no longer generate a machine XML file with a "state" attribute (because it's
* encoded inside the "parms" attribute), the XSL file must still cope with "state" attributes inside
* other XML files; for example, manifest XML files like /apps/pc/1981/visicalc/manifest.xml contain
* machine elements with "state" attributes that must still be passed down to the computer element
* "the old fashioned way".
*
* Until/unless that changes, components.xsl cannot be simplified as much as I might have hoped.
*/
if (typeof resources == 'object') sURL = null; // turn off URL inclusion if we have embedded resources
sXML = sXML.replace(/(<machine[^>]*\sid=)(['"]).*?\2/, "$1$2" + idMachine + "$2" + (sParms? " parms='" + sParms + "'" : "") + (sURL? ' url="' + sURL + '"' : ''));
}
if (!fResolve) {
/*
* I'm trying to switch to a shared components.xsl (at least for all PC-class machines),
* but in the interim, that means hacking the XSL file on the fly to reflect the actual class.
*/
sXML = sXML.replace(/(<xsl:variable name="APPNAME">).*?(<\/xsl:variable>)/, "$1" + sAppName + "$2");
sXML = sXML.replace(/(<xsl:variable name="APPCLASS">).*?(<\/xsl:variable>)/, "$1" + sAppClass + "$2");
/*
* 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>`);
}
}
/*
* 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 /devices/pc/machine/5150/cga/64kb/donkey/machine.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.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;
if (sXML.charAt(0) == '<') {
try {
/*
* Another hack for MSIE, which fails to load XSL documents containing a <!DOCTYPE [...]> tag.
*
* This is also why the XSLTProcessor 'transformToFragment' method in Microsoft Edge silently failed,
* so I had pull this hack out of the "ActiveXObject" code. And rather than add yet-another Microsoft
* browser check, I'm going to try doing this across the board, and hope that none of the other XSLT
* processors fail *without* the DOCTYPE tag.
*/
if (!fResolve) {
sXML = sXML.replace(/<!DOCTYPE(.|[\r\n])*]>\s*/g, "");
}
/*
* Beginning with Microsoft Edge and the corresponding release of Windows 10, all the
* 'ActiveXObject' crud has gone away; but of course, this code must remain in place if
* we want to continue supporting older Internet Explorer browsers (ie, back to IE9).
*/
/** @namespace window.ActiveXObject */
if (window.ActiveXObject || 'ActiveXObject' in window) { // second test is required for IE11 on Windows 8.1
xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM");
xmlDoc.async = false;
xmlDoc['loadXML'](sXML);
} else {
/** @namespace window.DOMParser */
xmlDoc = (new window.DOMParser()).parseFromString(sXML, "text/xml");
}
} catch(e) {
xmlDoc = null;
sXML = e.message;
}
} else {
sXML = "unrecognized XML: " + (sXML.length > 255? sXML.substr(0, 255) + "..." : sXML);
}
done(sXML, xmlDoc);
};
if (sXML) {
if (PRIVATE) sXML = sXML.replace(/\/library.xml/, "/private/library.xml");
if (fResolve) {
resolveXML(sXML, display, buildXML);
return;
}
buildXML(sXML, null);
return;
}
done("no data" + (sXMLFile? " for file: " + sXMLFile : ""), null);
}
/**
* resolveXML(sXML, display, done)
*
* Replaces every tag with a "ref" attribute with the contents of the corresponding file.
*
* TODO: Fix some of the limitations of this code, such as: 1) requiring the "ref" attribute
* to appear as the tag's first attribute, 2) requiring the "ref" attribute to be double-quoted,
* and 3) requiring the "ref" tag to be self-closing.
*
* @param {string} sXML
* @param {function(string)} display
* @param {function(string,(string|null))} done (the first string contains the resolved XML data, the second is for any error message)
*/
function resolveXML(sXML, display, done)
{
var matchRef;
var reRef = /<([a-z]+)\s+ref="(.*?)"(.*?)\/>/g;
if ((matchRef = reRef.exec(sXML))) {
var sRefFile = matchRef[2];
var doneReadXML = function(sURLName, sXMLRef, nErrorCode) {
if (nErrorCode || !sXMLRef) {
done(sXML, "unable to resolve XML reference: " + matchRef[0] + " (" + nErrorCode + ")");
return;
}
/*
* If there are additional attributes in the "referring" XML tag, we want to insert them
* into the "referred" XML tag; attributes that don't exist in the referred tag should be
* appended, and attributes that DO exist should be overwritten.
*/
var sRefAttrs = matchRef[3];
if (sRefAttrs) {
var aXMLRefTag = sXMLRef.match(new RegExp("<" + matchRef[1] + "[^>]*>"));
if (aXMLRefTag) {
var sXMLNewTag = aXMLRefTag[0];
/*
* Iterate over all the attributes in the "referring" XML tag (sRefAttrs)
*/
var matchAttr;
var reAttr = /( [a-z]+=)(['"])(.*?)\2/g;
while ((matchAttr = reAttr.exec(sRefAttrs))) {
if (sXMLNewTag.indexOf(matchAttr[1]) < 0) {
/*
* This is the append case
*/
sXMLNewTag = sXMLNewTag.replace(">", matchAttr[0] + ">");
} else {
/*
* This is the overwrite case
*/
sXMLNewTag = sXMLNewTag.replace(new RegExp(matchAttr[1] + "(['\"])(.*?)\\1"), matchAttr[0]);
}
}
if (aXMLRefTag[0] != sXMLNewTag) {
sXMLRef = sXMLRef.replace(aXMLRefTag[0], sXMLNewTag);
}
} else {
done(sXML, "missing <" + matchRef[1] + "> in " + sRefFile);
return;
}
}
/*
* Apparently when a Windows Azure server delivers one of my XML files, it may modify the first line:
*
* <?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);
resolveXML(sXML, display, done);
};
display("Loading " + sRefFile + "...");
Web.getResource(sRefFile, null, fAsync, doneReadXML);
return;
}
done(sXML, null);
}
/**
* embedMachine(sAppName, sAppClass, sVersion, idMachine, sXMLFile, sXSLFile, sParms)
*
* This allows to you embed a machine on a web page, by transforming the machine XML into HTML.
*
* @param {string} sAppName is the app name (eg, "PCx86")
* @param {string} sAppClass is the app class (eg, "pcx86"); also known as the machine class
* @param {string} sVersion is the app version (eg, "1.15.7")
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @param {string} [sParms]
* @return {boolean} true if successful, false if error
*/
function embedMachine(sAppName, sAppClass, sVersion, idMachine, sXMLFile, sXSLFile, sParms)
{
var eMachine, eWarning, fSuccess = true;
cAsyncMachines++;
Component.addMachine(idMachine);
var doneMachine = function() {
Component.assert(cAsyncMachines > 0);
if (!--cAsyncMachines) {
if (fAsync) Web.enablePageEvents(true);
}
};
var displayError = function(sError) {
Component.log(sError);
displayMessage("Error: " + sError);
if (fSuccess) doneMachine();
fSuccess = false;
};
var displayMessage = function(sMessage) {
if (eWarning === undefined) {
/*
* Our MarkOut module (in convertMDMachineLinks()) creates machine containers that look like:
*
* <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 embedXXX() call.
*/
var aeWarning = (eMachine && Component.getElementsByClass(eMachine, "machine-warning"));
eWarning = (aeWarning && aeWarning[0]) || eMachine;
}
if (eWarning) eWarning.innerHTML = Str.escapeHTML(sMessage);
};
try {
eMachine = document.getElementById(idMachine);
if (eMachine) {
/*
* If we have a 'css' resource, add it to the page first.
*/
var css;
if (typeof resources == "object" && (css = resources['css'])) {
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
head.appendChild(style);
}
if (!sXSLFile) {
/*
* Now that PCjs is an open-source project, we can make the following test more flexible,
* and revert to the internal template if DEBUG *or* internal version (instead of *and*).
*
* Third-party sites that don't use the PCjs server will ALWAYS want to specify a fully-qualified
* path to the XSL file, unless they choose to mirror our folder structure.
*/
var sAppFolder = sAppClass;
if (DEBUG || sVersion == "1.x.x") {
sXSLFile = "/modules/" + sAppFolder + "/templates/components.xsl";
} else {
if (sAppClass.substr(0, 3) == "pdp") sAppFolder = "pdpjs";
sXSLFile = "/versions/" + sAppFolder + "/" + sVersion + "/components.xsl";
}
}
var processXML = function(sXML, xml) {
if (!xml) {
displayError(sXML);
return;
}
/*
* Non-COMPILED kludge to extract the version number from the stylesheet path in the machine XML file;
* we don't need this code in COMPILED (non-DEBUG) releases, because APPVERSION is hard-coded into them.
*/
if (!COMPILED) {
var aMatch = sXML.match(/<\?xml-stylesheet[^>]* href=(['"])[^'"]*?\/([0-9.]*)\/([^'"]*)\1/);
if (aMatch) XMLVERSION = aMatch[2];
}
var transformXML = function(sXSL, xsl) {
if (!xsl) {
displayError(sXSL);
return;
}
/*
* Record the XSL file, in case someone wants to save the entire machine later.
*/
Component.addMachineResource(idMachine, sXSLFile, sXSL);
/*
* 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).
*/
displayMessage("Processing " + sXMLFile + "...");
/*
* Beginning with Microsoft Edge and the corresponding release of Windows 10, all the
* 'ActiveXObject' crud has gone away; but of course, this code must remain in place if
* we want to continue supporting older Internet Explorer browsers (ie, back to IE9).
*/
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;
doneMachine();
} else {
displayError("transformNodeToObject failed");
}
}
else if (document.implementation && document.implementation.createDocument) {
var xsltProcessor = new XSLTProcessor();
xsltProcessor['importStylesheet'](xsl);
var eFragment = xsltProcessor['transformToFragment'](xml, document);
if (eFragment) {
/*
* This fails in Microsoft Edge...
*
var machine = eFragment.getElementById(idMachine);
if (!machine) {
displayError("machine generation failed: " + idMachine);
} else
*/
if (eMachine.parentNode) {
eMachine.parentNode.replaceChild(eFragment, eMachine);
doneMachine();
} else {
/*
* NOTE: This error can occur if our Node web server, when processing a folder with
* both a manifest.xml with a machine.xml reference AND a README.md containing a
* machine link, generates duplicate embedXXX() calls for the same machine; if the
* first embedXXX() call finds its target, subsequent calls for the same target will
* fail.
*
* Technically, such a folder is in a misconfigured state, but it happens, in part
* because when we switched to the Jekyll web server, we had to add machine links to
* all README.md files where we had previously relied on manifest.xml or machine.xml
* processing. This is because the Jekyll web server currently doesn't process XML
* files, nor is support for that likely to be added any time soon; it was a nice
* feature of the Node web server, but it's not clear that it's worth doing for Jekyll.
*/
displayError("invalid machine element: " + idMachine);
}
} else {
displayError("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.
*/
displayError("unable to transform XML: unsupported browser");
}
};
loadXML(sXSLFile, null, sAppName, sAppClass, null, false, displayMessage, transformXML);
};
if (sXMLFile.charAt(0) != '<') {
loadXML(sXMLFile, idMachine, sAppName, sAppClass, sParms, true, displayMessage, processXML);
} else {
parseXML(sXMLFile, null, idMachine, sAppName, sAppClass, sParms, false, displayMessage, processXML);
}
} else {
displayError("missing machine element: " + idMachine);
}
} catch(e) {
displayError(e.message);
}
return fSuccess;
}
/**
* embedC1P(idMachine, sXMLFile, sXSLFile)
*
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @return {boolean} true if successful, false if error
*/
function embedC1P(idMachine, sXMLFile, sXSLFile)
{
if (fAsync) Web.enablePageEvents(false);
return embedMachine("C1Pjs", "c1pjs", APPVERSION, idMachine, sXMLFile, sXSLFile);
}
/**
* embedPCx86(idMachine, sXMLFile, sXSLFile, sParms)
*
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @param {string} [sParms]
* @return {boolean} true if successful, false if error
*/
function embedPCx86(idMachine, sXMLFile, sXSLFile, sParms)
{
if (fAsync) Web.enablePageEvents(false);
return embedMachine("PCx86", "pcx86", APPVERSION, idMachine, sXMLFile, sXSLFile, sParms);
}
/**
* embedPC8080(idMachine, sXMLFile, sXSLFile, sParms)
*
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @param {string} [sParms]
* @return {boolean} true if successful, false if error
*/
function embedPC8080(idMachine, sXMLFile, sXSLFile, sParms)
{
if (fAsync) Web.enablePageEvents(false);
return embedMachine("PC8080", "pc8080", APPVERSION, idMachine, sXMLFile, sXSLFile, sParms);
}
/**
* embedPDP11(idMachine, sXMLFile, sXSLFile, sParms)
*
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @param {string} [sParms]
* @return {boolean} true if successful, false if error
*/
function embedPDP11(idMachine, sXMLFile, sXSLFile, sParms)
{
if (fAsync) Web.enablePageEvents(false);
return embedMachine("PDPjs", "pdp11", APPVERSION, idMachine, sXMLFile, sXSLFile, sParms);
}
/**
* Prevent the Closure Compiler from renaming functions we want to export, by adding them as global properties.
*/
if (APPNAME == "C1Pjs") {
window['embedC1P'] = embedC1P;
}
if (APPNAME == "PCx86") {
window['embedPC'] = embedPCx86; // WARNING: embedPC() deprecated as of v1.23.0
window['embedPCx86'] = embedPCx86;
}
if (APPNAME == "PC8080") {
window['embedPC8080'] = embedPC8080;
}
if (APPNAME == "PDPjs") {
window['embedPDP11'] = embedPDP11;
}
window['enableEvents'] = Web.enablePageEvents;
window['sendEvent'] = Web.sendPageEvent;

View file

@ -0,0 +1,34 @@
/**
* @fileoverview Externs used by PCjs and C1Pjs (for the Closure Compiler)
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @copyright © Jeff Parsons 2012-2016
*
* This file is part of PCjs, a computer emulation software project at <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 modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <http://pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"use strict";
var global;
var resources;
// var webkitAudioContext;

251
modules/shared/es6/keys.js Normal file
View file

@ -0,0 +1,251 @@
/**
* @fileoverview Defines browser keyboard constants.
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @copyright © Jeff Parsons 2012-2016
*
* This file is part of PCjs, a computer emulation software project at <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 modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <http://pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"use strict";
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;
* 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
* quoted property names, which is why I've 'unquoted' as many of them as possible. One solution
* would be to add mnemonics for all of them, not just the non-printable ones (eg, SPACE instead
* of ' ', AMP instead of '&', etc.)
*/
ASCII: {
BREAK: 0, CTRL_A: 1, CTRL_B: 2, CTRL_C: 3, CTRL_D: 4, CTRL_E: 5, CTRL_F: 6, CTRL_G: 7,
CTRL_H: 8, CTRL_I: 9, CTRL_J: 10, CTRL_K: 11, CTRL_L: 12, CTRL_M: 13, CTRL_N: 14, CTRL_O: 15,
CTRL_P: 16, CTRL_Q: 17, CTRL_R: 18, CTRL_S: 19, CTRL_T: 20, CTRL_U: 21, CTRL_V: 22, CTRL_W: 23,
CTRL_X: 24, CTRL_Y: 25, CTRL_Z: 26,
' ': 32, '!': 33, '"': 34, '#': 35, '$': 36, '%': 37, '&': 38, "'": 39,
'(': 40, ')': 41, '*': 42, '+': 43, ',': 44, '-': 45, '.': 46, '/': 47,
'0': 48, '1': 49, '2': 50, '3': 51, '4': 52, '5': 53, '6': 54, '7': 55,
'8': 56, '9': 57, ':': 58, ';': 59, '<': 60, '=': 61, '>': 62, '?': 63,
'@': 64, A: 65, B: 66, C: 67, D: 68, E: 69, F: 70, G: 71,
H: 72, I: 73, J: 74, K: 75, L: 76, M: 77, N: 78, O: 79,
P: 80, Q: 81, R: 82, S: 83, T: 84, U: 85, V: 86, W: 87,
X: 88, Y: 89, Z: 90, '[': 91, '\\':92, ']': 93, '^': 94, '_': 95,
'`': 96, a: 97, b: 98, c: 99, d: 100, e: 101, f: 102, g: 103,
h: 104, i: 105, j: 106, k: 107, l: 108, m: 109, n: 110, o: 111,
p: 112, q: 113, r: 114, s: 115, t: 116, u: 117, v: 118, w: 119,
x: 120, y: 121, z: 122, '{':123, '|':124, '}':125, '~':126, DEL: 127
},
/*
* Browser keyCodes we must pay particular attention to. For the most part, these are non-alphanumeric
* or function keys, some which may require special treatment (eg, preventDefault() if returning false on
* the initial keyDown event is insufficient).
*
* keyCodes for most common ASCII keys can simply use the appropriate ASCII code above.
*
* Most of these represent non-ASCII keys (eg, the LEFT arrow key), yet for some reason, browsers defined
* them using ASCII codes (eg, the LEFT arrow key uses the ASCII code for '%' or 37).
*/
KEYCODE: {
/* 0x08 */ BS: 8, // BACKSPACE
/* 0x09 */ TAB: 9,
/* 0x0A */ LF: 10, // LINE FEED (TODO: Determine if any key actually generates this)
/* 0x0D */ CR: 13, // CARRIAGE RETURN
/* 0x10 */ SHIFT: 16,
/* 0x11 */ CTRL: 17,
/* 0x12 */ ALT: 18,
/* 0x13 */ PAUSE: 19, // PAUSE/BREAK
/* 0x14 */ CAPS_LOCK: 20,
/* 0x1B */ ESC: 27,
/* 0x20 */ SPACE: 32,
/* 0x21 */ PGUP: 33,
/* 0x22 */ PGDN: 34,
/* 0x23 */ END: 35,
/* 0x24 */ HOME: 36,
/* 0x25 */ LEFT: 37,
/* 0x26 */ UP: 38,
/* 0x27 */ RIGHT: 39,
/* 0x27 */ FF_QUOTE: 39,
/* 0x28 */ DOWN: 40,
/* 0x2C */ FF_COMMA: 44,
/* 0x2C */ PRTSC: 44,
/* 0x2D */ INS: 45,
/* 0x2E */ DEL: 46,
/* 0x2E */ FF_PERIOD: 46,
/* 0x2F */ FF_SLASH: 47,
/* 0x30 */ ZERO: 48,
/* 0x31 */ ONE: 49,
/* 0x32 */ TWO: 50,
/* 0x33 */ THREE: 51,
/* 0x34 */ FOUR: 52,
/* 0x35 */ FIVE: 53,
/* 0x36 */ SIX: 54,
/* 0x37 */ SEVEN: 55,
/* 0x38 */ EIGHT: 56,
/* 0x39 */ NINE: 57,
/* 0x3B */ FF_SEMI: 59,
/* 0x3D */ FF_EQUALS: 61,
/* 0x5B */ CMD: 91, // aka WIN
/* 0x5B */ FF_LBRACK: 91,
/* 0x5C */ FF_BSLASH: 92,
/* 0x5D */ RCMD: 93, // aka MENU
/* 0x5D */ FF_RBRACK: 93,
/* 0x60 */ NUM_0: 96,
/* 0x60 */ NUM_INS: 96,
/* 0x60 */ FF_BQUOTE: 96,
/* 0x61 */ NUM_1: 97,
/* 0x61 */ NUM_END: 97,
/* 0x62 */ NUM_2: 98,
/* 0x62 */ NUM_DOWN: 98,
/* 0x63 */ NUM_3: 99,
/* 0x63 */ NUM_PGDN: 99,
/* 0x64 */ NUM_4: 100,
/* 0x64 */ NUM_LEFT: 100,
/* 0x65 */ NUM_5: 101,
/* 0x65 */ NUM_CENTER: 101,
/* 0x66 */ NUM_6: 102,
/* 0x66 */ NUM_RIGHT: 102,
/* 0x67 */ NUM_7: 103,
/* 0x67 */ NUM_HOME: 103,
/* 0x68 */ NUM_8: 104,
/* 0x68 */ NUM_UP: 104,
/* 0x69 */ NUM_9: 105,
/* 0x69 */ NUM_PGUP: 105,
/* 0x6A */ NUM_MUL: 106,
/* 0x6B */ NUM_ADD: 107,
/* 0x6D */ NUM_SUB: 109,
/* 0x6E */ NUM_DEL: 110, // aka PERIOD
/* 0x6F */ NUM_DIV: 111,
/* 0x70 */ F1: 112,
/* 0x71 */ F2: 113,
/* 0x72 */ F3: 114,
/* 0x73 */ F4: 115,
/* 0x74 */ F5: 116,
/* 0x75 */ F6: 117,
/* 0x76 */ F7: 118,
/* 0x77 */ F8: 119,
/* 0x78 */ F9: 120,
/* 0x79 */ F10: 121,
/* 0x7A */ F11: 122,
/* 0x7B */ F12: 123,
/* 0x90 */ NUM_LOCK: 144,
/* 0x91 */ SCROLL_LOCK: 145,
/* 0xAD */ FF_DASH: 173,
/* 0xBA */ SEMI: 186, // Firefox: 59
/* 0xBB */ EQUALS: 187, // Firefox: 61
/* 0xBC */ COMMA: 188, // Firefox: 44
/* 0xBD */ DASH: 189, // Firefox: 173
/* 0xBE */ PERIOD: 190, // Firefox: 46
/* 0xBF */ SLASH: 191, // Firefox: 47
/* 0xC0 */ BQUOTE: 192, // Firefox: 96
/* 0xDB */ LBRACK: 219, // Firefox: 91
/* 0xDC */ BSLASH: 220, // Firefox: 92
/* 0xDD */ RBRACK: 221, // Firefox: 93
/* 0xDE */ QUOTE: 222, // Firefox: 39
/* 0xE0 */ FF_CMD: 224, // Firefox only (used for both CMD and RCMD)
//
// The following biases use what I'll call Decimal Coded Binary or DCB (the opposite of BCD),
// where the thousands digit is used to store the sum of "binary" digits 1 and/or 2 and/or 4.
//
// Technically, that makes it DCO (Decimal Coded Octal), but then again, BCD should have really
// been called HCD (Hexadecimal Coded Decimal), so if "they" can take liberties, so can I.
//
// ONDOWN is a bias we add to browser keyCodes that we want to handle on "down" rather than on "press".
//
ONDOWN: 1000,
//
// ONRIGHT is a bias we add to browser keyCodes that need to check for a "right" location (default is "left")
//
ONRIGHT: 2000,
//
// FAKE is a bias we add to signal these are fake keyCodes corresponding to internal keystroke combinations.
// The actual values are for internal use only and merely need to be unique and used consistently.
//
FAKE: 4000
},
/*
* The set of values that a browser may store in the 'location' property of a keyboard event object
* which we also support.
*/
LOCATION: {
LEFT: 1,
RIGHT: 2,
NUMPAD: 3
}
};
/*
* Check the event object's 'location' property for a non-zero value for the following ONRIGHT keys.
*/
Keys.KEYCODE.NUM_CR = Keys.KEYCODE.CR + Keys.KEYCODE.ONRIGHT;
/*
* Maps "stupid" keyCodes to their "non-stupid" counterparts
*/
Keys.STUPID_KEYCODES = {};
Keys.STUPID_KEYCODES[Keys.KEYCODE.SEMI] = Keys.ASCII[';']; // 186 -> 59
Keys.STUPID_KEYCODES[Keys.KEYCODE.EQUALS] = Keys.ASCII['=']; // 187 -> 61
Keys.STUPID_KEYCODES[Keys.KEYCODE.COMMA] = Keys.ASCII[',']; // 188 -> 44
Keys.STUPID_KEYCODES[Keys.KEYCODE.DASH] = Keys.ASCII['-']; // 189 -> 45
Keys.STUPID_KEYCODES[Keys.KEYCODE.PERIOD] = Keys.ASCII['.']; // 190 -> 46
Keys.STUPID_KEYCODES[Keys.KEYCODE.SLASH] = Keys.ASCII['/']; // 191 -> 47
Keys.STUPID_KEYCODES[Keys.KEYCODE.BQUOTE] = Keys.ASCII['`']; // 192 -> 96
Keys.STUPID_KEYCODES[Keys.KEYCODE.LBRACK] = Keys.ASCII['[']; // 219 -> 91
Keys.STUPID_KEYCODES[Keys.KEYCODE.BSLASH] = Keys.ASCII['\\']; // 220 -> 92
Keys.STUPID_KEYCODES[Keys.KEYCODE.RBRACK] = Keys.ASCII[']']; // 221 -> 93
Keys.STUPID_KEYCODES[Keys.KEYCODE.QUOTE] = Keys.ASCII["'"]; // 222 -> 39
Keys.STUPID_KEYCODES[Keys.KEYCODE.FF_DASH] = Keys.ASCII['-'];
/*
* Maps unshifted keyCodes to their shifted counterparts; to be used when a shift-key is down.
* Alphabetic characters are handled in code, since they must also take CAPS_LOCK into consideration.
*/
Keys.SHIFTED_KEYCODES = {};
Keys.SHIFTED_KEYCODES[Keys.ASCII['1']] = Keys.ASCII['!'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['2']] = Keys.ASCII['@'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['3']] = Keys.ASCII['#'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['4']] = Keys.ASCII['$'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['5']] = Keys.ASCII['%'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['6']] = Keys.ASCII['^'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['7']] = Keys.ASCII['&'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['8']] = Keys.ASCII['*'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['9']] = Keys.ASCII['('];
Keys.SHIFTED_KEYCODES[Keys.ASCII['0']] = Keys.ASCII[')'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.SEMI] = Keys.ASCII[':'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.EQUALS] = Keys.ASCII['+'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.COMMA] = Keys.ASCII['<'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.DASH] = Keys.ASCII['_'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.PERIOD] = Keys.ASCII['>'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.SLASH] = Keys.ASCII['?'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.BQUOTE] = Keys.ASCII['~'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.LBRACK] = Keys.ASCII['{'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.BSLASH] = Keys.ASCII['|'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.RBRACK] = Keys.ASCII['}'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.QUOTE] = Keys.ASCII['"'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.FF_DASH] = Keys.ASCII['_'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.FF_EQUALS] = Keys.ASCII['+'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.FF_SEMI] = Keys.ASCII[':'];
export default Keys;

View file

@ -0,0 +1,390 @@
/**
* @fileoverview Net-related functions
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a> (@jeffpar)
* @copyright © Jeff Parsons 2012-2016
*
* This file is part of PCjs, a computer emulation software project at <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 modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <http://pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"use strict";
import fs from "fs";
import http from "http";
import path from "path";
import url from "url";
import Str from "../../shared/es6/strlib";
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)
*
* @param {string} sPath
* @return {boolean} true if sPath is a (supported) remote path, false if not
*
* TODO: Add support for FTP? HTTPS? Anything else?
*/
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)
*
* @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?
*/
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,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)) {
console.log('Net.getResource("' + sURL + '"): unimplemented');
} else {
if (!Net.sServerRoot) {
Net.sServerRoot = path.join(path.dirname(fs.realpathSync(__filename)), "../../../");
}
/*
* TODO: Revisit why we pass back sBaseName instead of the original sURL....
*/
var sBaseName = Str.getBaseName(sURL);
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(sBaseName, 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(sBaseName, sResource, nErrorCode);
response = [sResource, nErrorCode];
}
}
return response;
}
}
/*
* 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 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.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.
*/
Net.asPropagate = [Net.GORT_COMMAND, "autostart"];
Net.sServerRoot = null;
export default Net;

View file

@ -0,0 +1,39 @@
/**
* @fileoverview Compile-time definitions for non-DEBUG configurations.
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @copyright © Jeff Parsons 2012-2016
*
* This file is part of PCjs, a computer emulation software project at <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 modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <http://pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"use strict";
/*
* 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 immediately after defines.js,
* which will then set DEBUG to false at runtime.
*/
DEBUG = false;

View file

@ -0,0 +1,39 @@
/**
* @fileoverview Definitions for PRIVATE configurations.
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @copyright © Jeff Parsons 2012-2016
*
* This file is part of PCjs, a computer emulation software project at <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 modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <http://pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"use strict";
/*
* 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
* the default.
*
* Currently, the only (checked-in) use of the private setting is to set the client's PRIVATE global and trigger
* the loading of alternate (ie, private) XML files in embed.js.
*/
PRIVATE = true;

View file

@ -0,0 +1,87 @@
/**
* @fileoverview Process-related helper functions
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a> (@jeffpar)
* @copyright © Jeff Parsons 2012-2016
*
* This file is part of PCjs, a computer emulation software project at <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 modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <http://pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"use strict";
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 = 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};
}
}
export default Proc;

View file

@ -0,0 +1,49 @@
/**
* @fileoverview Report API, as defined by httpapi.js
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @copyright © Jeff Parsons 2012-2016
*
* This file is part of PCjs, a computer emulation software project at <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 modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <http://pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"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"
}
};
export default ReportAPI;

219
modules/shared/es6/save.js Normal file
View file

@ -0,0 +1,219 @@
/**
* @fileoverview PCjs save functionality.
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @copyright © Jeff Parsons 2012-2016
*
* This file is part of PCjs, a computer emulation software project at <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 modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <http://pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"use strict";
import Str from "../../shared/es6/strlib";
import Web from "../../shared/es6/weblib";
import Component from "../../shared/es6/component";
/**
* savePC(idMachine, sPCJSFile, callback)
*
* @param {string} idMachine
* @param {string} sPCJSFile
* @param {function(Object)} [callback]
* @return {boolean} true if successful, false if error
*/
function savePC(idMachine, sPCJSFile, callback)
{
var cmp = /** @type {Computer} */ (Component.getComponentByType("Computer", idMachine));
var dbg = /** @type {Debugger} */ (Component.getComponentByType("Debugger", idMachine));
if (cmp) {
var sState = cmp.powerOff(true);
var sParms = cmp.saveMachineParms();
if (!sPCJSFile) {
if (DEBUG) {
sPCJSFile = "/tmp/pcjs/" + (XMLVERSION || APPVERSION) + "/pc.js"
} else {
sPCJSFile = "/versions/pcjs/" + (XMLVERSION || APPVERSION) + "/pc" + (dbg? "-dbg" : "") + ".js";
}
}
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]);
});
return true;
}
Web.alertUser("Unable to identify machine '" + idMachine + "'");
return false;
}
/**
* downloadCSS(sURL, sPCJS, nErrorCode, aMachineInfo)
*
* @param {string} sURL
* @param {string} sPCJS
* @param {number} nErrorCode
* @param {Array} aMachineInfo ([0] = idMachine, [1] = sScript, [2] = sParms, [3] = sState)
*/
function downloadCSS(sURL, sPCJS, nErrorCode, aMachineInfo)
{
if (!nErrorCode && sPCJS) {
aMachineInfo.push(sPCJS);
var res = Component.getMachineResources(aMachineInfo[0]);
var sCSSFile = null;
for (var sName in res) {
if (Str.endsWith(sName, "components.xsl")) {
sCSSFile = sName.replace(".xsl", ".css");
break;
}
}
if (!sCSSFile) {
/*
* This is probably a bad idea (ie, allowing downloadPC() to proceed with our stylesheet)...
*/
downloadPC(sURL, null, 0, aMachineInfo);
} else {
Web.getResource(sCSSFile, null, true, function(sURL, sResponse, nErrorCode) {
downloadPC(sURL, sResponse, nErrorCode, aMachineInfo);
});
}
return;
}
Web.alertUser("Error (" + nErrorCode + ") requesting " + sURL);
}
/**
* downloadPC(sURL, sCSS, nErrorCode, aMachineInfo)
*
* @param {string} sURL
* @param {string|null} sCSS
* @param {number} nErrorCode
* @param {Array} aMachineInfo ([0] = idMachine, [1] = sScript, [2] = sParms, [3] = sState, [4] = sPCJS)
*/
function downloadPC(sURL, sCSS, nErrorCode, aMachineInfo)
{
var matchScript, sXMLFile, sXSLFile;
var idMachine = aMachineInfo[0], sScript = aMachineInfo[1], sPCJS = aMachineInfo[4];
/*
* sPCJS is supposed to contain the entire PCjs script, which has been wrapped with:
*
* (function(){...
*
* at the top and:
*
* ...})();
*
* at the bottom, thanks to the following Closure Compiler option:
*
* --output_wrapper "(function(){%output%})();"
*
* Immediately inside that wrapping, we want to embed all the specified machine's resources, using:
*
* var resources = {"xml": "...", "xsl": "...", ...};
*
* Note that the "resources" variable has been added to our externs.js, to prevent it from being renamed
* by the Closure Compiler.
*/
matchScript = sPCJS.match(/^(\s*\(function\(\)\{)([\s\S]*)(}\)\(\);\s*)$/);
if (!matchScript) {
/*
* If the match failed, we assume that a DEBUG (uncompiled) script is being used,
* so we'll provide a fake match that should work with whatever script was provided.
*/
if (DEBUG) {
matchScript = [sPCJS, "", sPCJS, ""];
} else {
sPCJS = "";
}
}
var resOld = Component.getMachineResources(idMachine), resNew = {};
for (var sName in resOld) {
var data = resOld[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
* other machine resources, and remove those entries.
*/
var matchDisk, reDisk = /[ \t]*<disk [^>]*path=(['"])(.*?)\1.*?<\/disk>\n?/g;
while (matchDisk = reDisk.exec(resOld[sName])) {
var path = matchDisk[2];
if (path) {
if (resOld[path]) {
Component.log("recording disk: '" + path + "'");
} else {
data = data.replace(matchDisk[0], "");
}
}
}
sXMLFile = sName = Str.getBaseName(sName);
}
else if (sExt == "xsl") {
sXSLFile = sName = Str.getBaseName(sName);
}
Component.log("saving resource: '" + sName + "' (" + data.length + " bytes)");
resNew[sName] = data;
}
if (sCSS) {
resNew[sName = 'css'] = sCSS;
Component.log("saving resource: '" + sName + "' (" + sCSS.length + " bytes)");
}
if (aMachineInfo[2]) {
var sParms = resNew[sName = 'parms'] = aMachineInfo[2];
Component.log("saving resource: '" + sName + "' (" + sParms.length + " bytes)");
}
if (aMachineInfo[3]) {
var sState = resNew[sName = 'state'] = aMachineInfo[3];
Component.log("saving resource: '" + sName + "' (" + sState.length + " bytes)");
}
if (sXMLFile && sXSLFile) {
var sResources = JSON.stringify(resNew);
sScript += ".js";
sPCJS = matchScript[1] + "var resources=" + sResources + ";" + matchScript[2] + matchScript[3];
Component.log("saving machine: '" + idMachine + "' (" + sPCJS.length + " bytes)");
sPCJS = sPCJS.replace(/\u00A9/g, "&#xA9;");
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';
sAlert += '...\n';
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);
return;
}
Web.alertUser("Missing XML/XSL resources");
}
/**
* Prevent the Closure Compiler from renaming functions we want to export, by adding them
* as (named) properties of a global object.
*/
window['savePC'] = savePC;

395
modules/shared/es6/state.js Normal file
View file

@ -0,0 +1,395 @@
/**
* @fileoverview The State class used by PCjs machines.
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @copyright © Jeff Parsons 2012-2016
*
* This file is part of PCjs, a computer emulation software project at <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 modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <http://pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"use strict";
import Web from "../../shared/es6/weblib";
import Component from "../../shared/es6/component";
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);
}
/**
* set(id, data)
*
* @this {State}
* @param {number|string} id
* @param {Object|string} data
*/
set(id, data)
{
try {
this.state[id] = data;
} catch(e) {
Component.log(e.message);
}
}
/**
* get(id)
*
* @this {State}
* @param {number|string} id
* @return {Object|string|null}
*/
get(id)
{
return this.state[id] || null;
}
/**
* data()
*
* @this {State}
* @return {Object}
*/
data()
{
return this.state;
}
/**
* 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 {string|null} [json]
* @return {boolean} true if state exists in localStorage, false if not
*/
load(json)
{
if (json) {
this.json = json;
this.fLoaded = true;
this.fParsed = false;
return true;
}
if (this.fLoaded) {
/*
* This is assumed to be a redundant load().
*/
return true;
}
if (Web.hasLocalStorage()) {
var s = Web.getLocalStorageItem(this.key);
if (s) {
this.json = s;
this.fLoaded = true;
if (DEBUG) Component.log("localStorage(" + this.key + "): " + s.length + " bytes loaded");
return true;
}
}
return false;
}
/**
* parse()
*
* This completes the load() operation, by parsing what was loaded, on the assumption there
* might be some benefit to deferring parsing until we've given the user a chance to confirm.
* Otherwise, load() could have just as easily done this, too.
*
* @this {State}
* @return {boolean} true if successful, false if error
*/
parse()
{
var fSuccess = true;
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()
{
var fSuccess = true;
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 {
/*
* WARNING: Because browsers tend to disable all alerts() during an "unload" operation,
* it's unlikely anyone will ever see the "quota" errors that occur at this point. Need to
* think of some way to notify the user that there's a problem, and offer a way of cleaning
* up old states.
*/
Component.error("Unable to store " + s.length + " bytes in browser local storage");
fSuccess = false;
}
}
return fSuccess;
}
/**
* toString()
*
* @this {State}
* @return {string} JSON-encoded state
*/
toString()
{
return this.state? JSON.stringify(this.state) : this.json;
}
/**
* unload(parms)
*
* This discards any data saved via set() or loaded via load(), creating an empty State object.
* Note that you have to follow this call with an explicit call to store() if you want to remove
* the state from localStorage as well.
*
* @this {State}
* @param {Object} [parms]
*/
unload(parms)
{
this.json = "";
this.state = {};
this.fLoaded = this.fParsed = false;
if (parms) this.set("parms", parms);
}
/**
* clear(fAll)
*
* This unloads the current state, and then clears ALL localStorage for the current machine,
* independent of version, to reduce the chance of orphaned states wasting part of our limited allocation.
*
* @this {State}
* @param {boolean} [fAll] true to unconditionally clear ALL localStorage for the current domain
*/
clear(fAll)
{
this.unload();
var aKeys = Web.getLocalStorageKeys();
for (var i = 0; i < aKeys.length; i++) {
var sKey = aKeys[i];
if (sKey && (fAll || sKey.substr(0, this.key.length) == this.key)) {
Web.removeLocalStorageItem(sKey);
if (DEBUG) 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;
}
}
export default State;

View file

@ -0,0 +1,620 @@
/**
* @fileoverview String-related helper functions
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a> (@jeffpar)
* @copyright © Jeff Parsons 2012-2016
*
* This file is part of PCjs, a computer emulation software project at <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 modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <http://pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"use strict";
class Str {
/**
* isValidInt(s, base)
*
* The built-in parseInt() function has the annoying feature of returning a partial value (ie,
* up to the point where it encounters an invalid character); eg, parseInt("foo", 16) returns 0xf.
*
* So it's best to use our own Str.parseInt() function, which will in turn use this function to
* validate the entire string.
*
* @param {string} s is the string representation of some number
* @param {number} [base] is the radix to use (default is 10); only 2, 8, 10 and 16 are supported
* @return {boolean} true if valid, false if invalid (or the specified base isn't supported)
*/
static isValidInt(s, base)
{
if (!base || base == 10) return s.match(/^[0-9]+$/) !== null;
if (base == 16) return s.match(/^[0-9a-f]+$/i) !== null;
if (base == 8) return s.match(/^[0-7]+$/) !== null;
if (base == 2) return s.match(/^[01]+$/) !== null;
return false;
}
/**
* parseInt(s, base)
*
* This is a wrapper around the built-in parseInt() function. Our wrapper recognizes certain prefixes
* ('$' or "0x" for hex, '#' or "0o" for octal) and suffixes ('.' for decimal, 'h' for hex, 'y' for
* binary), and then calls isValidInt() to ensure we don't convert strings that contain partial values;
* see isValidInt() for details.
*
* The use of multiple prefix/suffix combinations is undefined (although for the record, we process
* prefixes first). We do NOT support the "0b" prefix to indicate binary UNLESS one or more commas are
* also present (because "0b" is also a valid hex sequence), and we do NOT support a single leading zero
* to indicate octal (because such a number could also be decimal or hex). Any number of commas are
* allowed; we remove them all before calling the built-in parseInt().
*
* To summarize our non-standard alternatives: a 'y' suffix indicates binary, a '#' prefix indicates
* octal, a '$' prefix indicates hex, and a "0b" prefix indicates binary IF at least one comma is present.
* Commas are useful for grouping binary digits, but if you don't want to use them, then you must use a
* 'y' suffix for binary numbers.
*
* @param {string} s is the string representation of some number
* @param {number} [base] is the radix to use (default is 10); can be overridden by prefixes/suffixes
* @return {number|undefined} corresponding value, or undefined if invalid
*/
static parseInt(s, base)
{
var value;
if (s) {
if (!base) base = 10;
var chPrefix = s.charAt(0);
var fCommas = (s.indexOf(',') > 0);
if (fCommas) s = s.replace(/,/g, '');
if (chPrefix == '#') {
base = 8;
chPrefix = null;
}
else if (chPrefix == '$') {
base = 16;
chPrefix = null;
}
if (chPrefix == null) {
s = s.substr(1);
}
else {
if (chPrefix == '0') {
chPrefix = s.charAt(1);
if (chPrefix == 'b' && fCommas) {
base = 2;
chPrefix = null;
}
if (chPrefix == 'o') {
base = 8;
chPrefix = null;
}
else if (chPrefix == 'x') {
base = 16;
chPrefix = null;
}
}
if (chPrefix == null) {
s = s.substr(2);
}
else {
var chSuffix = s.charAt(s.length - 1).toLowerCase();
if (chSuffix == 'y') {
base = 2;
chSuffix = null;
}
else if (chSuffix == '.') {
base = 10;
chSuffix = null;
}
else if (chSuffix == 'h') {
base = 16;
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 | 0;
}
}
return value;
}
/**
* toBin(n, cch, grouping)
*
* Converts an integer to binary, with the specified number of digits (up to the default of 32).
*
* @param {number|null|undefined} n is a 32-bit value
* @param {number} [cch] is the desired number of binary digits (32 is both the default and the maximum)
* @param {number} [grouping]
* @return {string} the binary representation of n
*/
static toBin(n, cch, grouping)
{
var s = "";
if (!cch) {
cch = 32;
} else {
if (cch > 32) cch = 32;
}
/*
* An initial "falsey" check for null takes care of both null and undefined;
* we can't rely entirely on isNaN(), because isNaN(null) returns false, oddly enough.
*
* Alternatively, we could mask and shift n regardless of whether it's null/undefined/NaN,
* since JavaScript coerces such operands to zero, but I think there's "value" in seeing those
* values displayed differently.
*/
var fInvalid = (n == null || isNaN(n));
var group = (grouping = grouping || cch);
while (cch-- > 0) {
if (!group) {
s = "," + s;
group = grouping;
}
s = (fInvalid ? '?' : ((n & 0x1) ? '1' : '0')) + s;
n >>= 1;
group--;
}
return s;
}
/**
* toBinBytes(n, cb, fPrefix)
*
* Converts an integer to binary, with the specified number of bytes (up to the default of 4).
*
* @param {number|null|undefined} n is a 32-bit value
* @param {number} [cb] is the desired number of binary bytes (4 is both the default and the maximum)
* @param {boolean} [fPrefix]
* @return {string} the binary representation of n
*/
static toBinBytes(n, cb, fPrefix)
{
var s = "";
if (!cb || cb > 4) cb = 4;
for (var i = 0; i < cb; i++) {
if (s) s = ',' + s;
s = Str.toBin(n & 0xff, 8) + s;
n >>= 8;
}
return (fPrefix ? "0b" : "") + s;
}
/**
* toOct(n, cch, fPrefix)
*
* Converts an integer to octal, with the specified number of digits (default of 6; max of 11)
*
* You might be tempted to use the built-in n.toString(8) instead, but it doesn't zero-pad and it
* doesn't properly convert negative values. Moreover, if n is undefined, n.toString() will throw
* an exception, whereas this function will return '?' characters.
*
* @param {number|null|undefined} n is a 32-bit value
* @param {number} [cch] is the desired number of octal digits (0 or undefined for default of either 6 or 11)
* @param {boolean} [fPrefix]
* @return {string} the octal representation of n
*/
static toOct(n, cch, fPrefix)
{
var s = "";
if (cch) {
if (cch > 11) cch = 11;
} else {
cch = (n & ~0xffffff) ? 11 : ((n & ~0xffff) ? 8 : 6);
}
/*
* An initial "falsey" check for null takes care of both null and undefined;
* we can't rely entirely on isNaN(), because isNaN(null) returns false, oddly enough.
*
* Alternatively, we could mask and shift n regardless of whether it's null/undefined/NaN,
* since JavaScript coerces such operands to zero, but I think there's "value" in seeing those
* values displayed differently.
*/
if (n == null || isNaN(n)) {
while (cch-- > 0) s = '?' + s;
} else {
while (cch-- > 0) {
var d = (n & 7) + 0x30;
s = String.fromCharCode(d) + s;
n >>= 3;
}
}
return (fPrefix ? "0o" : "") + s;
}
/**
* toDec(n, cch)
*
* Converts an integer to decimal, with the specified number of digits (default of 5; max of 10)
*
* You might be tempted to use the built-in n.toString(10) instead, but it doesn't zero-pad and it
* doesn't properly convert negative values. Moreover, if n is undefined, n.toString() will throw
* an exception, whereas this function will return '?' characters.
*
* @param {number|null|undefined} n is a 32-bit value
* @param {number} [cch] is the desired number of decimal digits (0 or undefined for default of either 5 or 10)
* @return {string} the octal representation of n
*/
static toDec(n, cch)
{
var s = "";
if (cch) {
if (cch > 10) cch = 10;
} else {
cch = (n & ~0xffff) ? 10 : 5;
}
/*
* An initial "falsey" check for null takes care of both null and undefined;
* we can't rely entirely on isNaN(), because isNaN(null) returns false, oddly enough.
*
* Alternatively, we could mask and shift n regardless of whether it's null/undefined/NaN,
* since JavaScript coerces such operands to zero, but I think there's "value" in seeing those
* values displayed differently.
*/
if (n == null || isNaN(n)) {
while (cch-- > 0) s = '?' + s;
} else {
while (cch-- > 0) {
var d = (n % 10) + 0x30;
s = String.fromCharCode(d) + s;
n /= 10;
}
}
return s;
}
/**
* toHex(n, cch, fPrefix)
*
* Converts an integer to hex, with the specified number of digits (default of 4 or 8, max 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 this function will 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 null/undefined/NaN values:
*
* s = (n < 0? n + 0x100000000 : n).toString(16);
* s = "00000000".substr(0, 8 - s.length) + s;
* s = s.substr(0, cch).toUpperCase();
*
* @param {number|null|undefined} n is a 32-bit value
* @param {number} [cch] is the desired number of hex digits (0 or undefined for default of either 4 or 8)
* @param {boolean} [fPrefix]
* @return {string} the hex representation of n
*/
static toHex(n, cch, fPrefix)
{
var s = "";
if (cch) {
if (cch > 8) cch = 8;
} else {
cch = (n & ~0xffff) ? 8 : 4;
}
/*
* An initial "falsey" check for null takes care of both null and undefined;
* we can't rely entirely on isNaN(), because isNaN(null) returns false, oddly enough.
*
* Alternatively, we could mask and shift n regardless of whether it's null/undefined/NaN,
* since JavaScript coerces such operands to zero, but I think there's "value" in seeing those
* values displayed differently.
*/
if (n == null || isNaN(n)) {
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 (fPrefix ? "0x" : "") + s;
}
/**
* toHexByte(b)
*
* Alias for Str.toHex(b, 2, true)
*
* @param {number|null|undefined} b is a byte value
* @return {string} the hex representation of b
*/
static toHexByte(b)
{
return Str.toHex(b, 2, true);
}
/**
* toHexWord(w)
*
* Alias for Str.toHex(w, 4, true)
*
* @param {number|null|undefined} w is a word (16-bit) value
* @return {string} the hex representation of w
*/
static toHexWord(w)
{
return Str.toHex(w, 4, true);
}
/**
* toHexLong(l)
*
* Alias for Str.toHex(l, 8, true)
*
* @param {number|null|undefined} l is a dword (32-bit) value
* @return {string} the hex representation of w
*/
static toHexLong(l)
{
return Str.toHex(l, 8, true);
}
/**
* getBaseName(sFileName, fStripExt)
*
* This is a poor-man's version of Node's path.basename(), which Node-only components should use instead.
*
* Note that if fStripExt is true, this strips ANY extension, whereas path.basename() strips 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}
*/
static getBaseName(sFileName, fStripExt)
{
var sBaseName = sFileName;
var i = sFileName.lastIndexOf('/');
if (i >= 0) sBaseName = sFileName.substr(i + 1);
/*
* This next bit is a kludge to clean up names that are part of a URL that includes unsightly query parameters.
*/
i = sBaseName.indexOf('&');
if (i > 0) sBaseName = sBaseName.substr(0, i);
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
*/
static getExtension(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
*/
static endsWith(s, sSuffix)
{
return s.indexOf(sSuffix, s.length - sSuffix.length) !== -1;
}
/**
* escapeHTML(sHTML)
*
* @param {string} sHTML
* @return {string} with HTML entities "escaped", similar to PHP's htmlspecialchars()
*/
static escapeHTML(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}
*/
static replaceAll(sFind, sReplace, s)
{
var a = {};
a[sFind] = sReplace;
return Str.replaceArray(a, s);
}
/**
* replaceArray(a, s)
*
* @param {Object} a
* @param {string} s
* @return {string}
*/
static replaceArray(a, s)
{
var sMatch = "";
for (var k in a) {
/*
* As noted in:
*
* http://www.regexguru.com/2008/04/escape-characters-only-when-necessary/
*
* inside character classes, only backslash, caret, hyphen and the closing bracket need to be
* escaped. And in fact, if you ensure that the closing bracket is first, the caret is not first,
* and the hyphen is last, you can avoid escaping those as well.
*/
k = k.replace(/([\\[\]*{}().+?])/g, "\\$1");
sMatch += (sMatch ? '|' : '') + k;
}
return s.replace(new RegExp('(' + sMatch + ')', "g"), function (m)
{
return a[m];
});
}
/**
* pad(s, cch, fPadLeft)
*
* NOTE: the maximum amount of padding currently supported is 40 spaces.
*
* @param {string} s is a string
* @param {number} cch is desired length
* @param {boolean} [fPadLeft] (default is padding on the right)
* @return {string} the original string (s) with spaces padding it to the specified length
*/
static pad(s, cch, fPadLeft)
{
var sPadding = " ";
return fPadLeft ? (sPadding + s).slice(-cch) : (s + sPadding).slice(0, cch);
}
/**
* stripLeadingZeros(s, fPad)
*
* @param {string} s
* @param {boolean} [fPad]
* @return {string}
*/
static stripLeadingZeros(s, fPad)
{
var cch = s.length;
s = s.replace(/^0+([0-9A-F]+)$/i, "$1");
if (fPad) s = Str.pad(s, cch, true);
return s;
}
/**
* trim(s)
*
* @param {string} s
* @return {string}
*/
static trim(s)
{
if (String.prototype.trim) {
return s.trim();
}
return s.replace(/^\s+|\s+$/g, "");
}
/**
* toASCIICode(b)
*
* @param {number} b
* @return {string}
*/
static toASCIICode(b)
{
var s = (b != Str.ASCII.CR && b != Str.ASCII.LF ? Str.aASCIICodes[b] : null);
if (s) {
s = '<' + s + '>';
} else {
s = String.fromCharCode(b);
}
return s;
}
}
Str.aHTMLEscapeMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
/*
* Future home of a general-purpose ASCII table. TODO: Flesh it out.
*/
Str.ASCII = {
LF: 0x0A,
CR: 0x0D
};
/*
* Table for converting "unprintable" ASCII codes into mnemonics, to more clearly see what's being printed.
*/
Str.aASCIICodes = {
0x00: "NUL",
0x01: "SOH", // (CTRL_A) Start of Heading
0x02: "STX", // (CTRL_B) Start of Text
0x03: "ETX", // (CTRL_C) End of Text
0x04: "EOT", // (CTRL_D) End of Transmission
0x05: "ENQ", // (CTRL_E) Enquiry
0x06: "ACK", // (CTRL_F) Acknowledge
0x07: "BEL", // (CTRL_G) Bell
0x08: "BS", // (CTRL_H) Backspace
0x09: "TAB", // (CTRL_I) Horizontal Tab
0x0A: "LF", // (CTRL_J) Line Feed (New Line)
0x0B: "VT", // (CTRL_K) Vertical Tab
0x0C: "FF", // (CTRL_L) Form Feed (New Page)
0x0D: "CR", // (CTRL_M) Carriage Return
0x0E: "SO", // (CTRL_N) Shift Out
0x0F: "SI", // (CTRL_O) Shift In
0x10: "DLE", // (CTRL_P) Data Link Escape
0x11: "XON", // (CTRL_Q) Device Control 1 (aka DC1)
0x12: "DC2", // (CTRL_R) Device Control 2
0x13: "XOFF", // (CTRL_S) Device Control 3 (aka DC3)
0x14: "DC4", // (CTRL_T) Device Control 4
0x15: "NAK", // (CTRL_U) Negative Acknowledge
0x16: "SYN", // (CTRL_V) Synchronous Idle
0x17: "ETB", // (CTRL_W) End of Transmission Block
0x18: "CAN", // (CTRL_X) Cancel
0x19: "EM", // (CTRL_Y) End of Medium
0x1A: "SUB", // (CTRL_Z) Substitute
0x1B: "ESC", // Escape
0x1C: "FS", // File Separator
0x1D: "GS", // Group Separator
0x1E: "RS", // Record Separator
0x1F: "US" // Unit Separator
};
export default Str;

View file

@ -0,0 +1,68 @@
/**
* @fileoverview User API, as defined by httpapi.js
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @copyright © Jeff Parsons 2012-2016
*
* This file is part of PCjs, a computer emulation software project at <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 modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <http://pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"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"
}
};
export default UserAPI;

View file

@ -0,0 +1,324 @@
/**
* @fileoverview Assorted helper functions
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a> (@jeffpar)
* @copyright © Jeff Parsons 2012-2016
*
* This file is part of PCjs, a computer emulation software project at <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 modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <http://pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"use strict";
/**
* @typedef {{
* mask: number,
* shift: number
* }}
*/
var BitField;
/**
* @typedef {Object.<BitField>}
*/
var BitFields;
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;
}
/**
* 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);
}
}
/**
* getTime()
*
* @return {number} the current time, in milliseconds
*/
static getTime()
{
return Date.now() || +new Date();
}
/**
* 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());
}
/**
* 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;
}
// Component.assert(bit <= 32);
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)
{
// 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}
*/
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];
export default Usr;

View file

@ -0,0 +1,987 @@
/**
* @fileoverview Browser-related helper functions
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a> (@jeffpar)
* @copyright © Jeff Parsons 2012-2016
*
* This file is part of PCjs, a computer emulation software project at <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 modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <http://pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"use strict";
import Component from "../../shared/es6/component";
import ReportAPI from "../../shared/es6/reportapi";
/*
* 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
*/
class Web {
/**
* log(s, type)
*
* For diagnostic output only. DEBUG must be true (or "--debug" specified via the command-line)
* for Component.log() to display anything.
*
* @param {string} [s] is the message text
* @param {string} [type] is the message type
*/
static log(s, type)
{
Component.log(s, type);
}
/**
* notice(s, fPrintOnly, id)
*
* If Component.notice() calls Web.alertUser(), it will fall back to Web.log() if all else fails.
*
* @param {string} s is the message text
* @param {boolean} [fPrintOnly]
* @param {string} [id] is the caller's ID, if any
*/
static notice(s, fPrintOnly, id)
{
Component.notice(s, fPrintOnly, id);
}
/**
* getResource(sURL, dataPost, fAsync, done)
*
* Request the specified resource (sURL), and once the request is complete, notify done().
*
* Also, if dataPost is set to a string, that string can be used to control the response format;
* by default, the response format is plain text, but you can specify "bytes" to request arbitrary
* binary data, which should come back as a string of bytes.
*
* TODO: The "bytes" option works by calling overrideMimeType(), which was never a best practice.
* Instead, we should implement supported response types ("text" and "arraybuffer", at a minimum)
* by setting xmlHTTP.responseType to one of those values before calling xmlHTTP.send().
*
* ES6 ALERT: Default parameters.
*
* @param {string} sURL
* @param {string|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
*/
static getResource(sURL, dataPost, fAsync = false, done)
{
var nErrorCode = 0, sResource = null, response = null;
if (typeof resources == 'object' && (sResource = resources[sURL])) {
if (done) done(sURL, sResource, nErrorCode);
return [sResource, nErrorCode];
}
else if (fAsync && typeof resources == 'function') {
resources(sURL, function (sResource, nErrorCode)
{
if (done) done(sURL, sResource, nErrorCode);
});
return response;
}
if (DEBUG) {
/*
* The larger resources we put on archive.pcjs.org should also be available locally...
*/
sURL = sURL.replace(/^http:\/\/archive.pcjs.org(\/.*)\/([^\/]*)$/, "$1/archive/$2");
}
var xmlHTTP = (window.XMLHttpRequest ? new window.XMLHttpRequest() : new window.ActiveXObject("Microsoft.XMLHTTP"));
if (fAsync) {
xmlHTTP.onreadystatechange = function ()
{
if (xmlHTTP.readyState === 4) {
/*
* The following line was 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;
*/
sResource = xmlHTTP.responseText;
/*
* The normal "success" case is an HTTP status code of 200, but when testing with files loaded
* from the local file system (ie, when using the "file:" protocol), we have to be a bit more "flexible".
*/
if (xmlHTTP.status == 200 || !xmlHTTP.status && sResource.length && Web.getHostProtocol() == "file:") {
if (MAXDEBUG) Web.log("xmlHTTP.onreadystatechange(" + sURL + "): returned " + sResource.length + " bytes");
}
else {
nErrorCode = xmlHTTP.status || -1;
Web.log("xmlHTTP.onreadystatechange(" + sURL + "): error code " + nErrorCode);
}
if (done) done(sURL, sResource, nErrorCode);
}
};
}
if (dataPost && typeof dataPost == "object") {
var sDataPost = "";
for (var p in dataPost) {
if (!dataPost.hasOwnProperty(p)) continue;
if (sDataPost) sDataPost += "&";
sDataPost += p + '=' + encodeURIComponent(dataPost[p]);
}
sDataPost = sDataPost.replace(/%20/g, '+');
if (MAXDEBUG) Web.log("Web.getResource(POST " + sURL + "): " + sDataPost.length + " bytes");
xmlHTTP.open("POST", sURL, fAsync); // ensure that fAsync is a valid boolean (Internet Explorer xmlHTTP functions insist on it)
xmlHTTP.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xmlHTTP.send(sDataPost);
} else {
if (MAXDEBUG) Web.log("Web.getResource(GET " + sURL + ")");
xmlHTTP.open("GET", sURL, fAsync); // ensure that fAsync is a valid boolean (Internet Explorer xmlHTTP functions insist on it)
if (dataPost == "bytes") {
xmlHTTP.overrideMimeType("text/plain; charset=x-user-defined");
}
xmlHTTP.send();
}
if (!fAsync) {
sResource = xmlHTTP.responseText;
if (xmlHTTP.status == 200) {
if (MAXDEBUG) Web.log("Web.getResource(" + sURL + "): returned " + sResource.length + " bytes");
} else {
nErrorCode = xmlHTTP.status || -1;
Web.log("Web.getResource(" + sURL + "): error code " + nErrorCode);
}
if (done) done(sURL, sResource, nErrorCode);
response = [sResource, nErrorCode];
}
return response;
}
/**
* parseMemoryResource(sURL, sData)
*
* @param {string} sURL
* @param {string} sData
* @return {Object|null} (resource)
*/
static parseMemoryResource(sURL, sData)
{
var i;
var resource = {
aBytes: null,
aSymbols: null,
addrLoad: null,
addrExec: null
};
if (sData.charAt(0) == "[" || sData.charAt(0) == "{") {
try {
var a, ib, data;
if (sData.substr(0, 1) == "<") { // if the "data" begins with a "<"...
/*
* Early server configs reported an error (via the nErrorCode parameter) if a tape URL was invalid,
* but more recent server configs now display a somewhat friendlier HTML error page. The downside,
* however, is that the original error has been buried, and we've received "data" that isn't actually
* tape data. So if the data we've received appears to be "HTML-like", we treat it as an error message.
*/
throw new Error(sData);
}
/*
* TODO: IE9 is rather unfriendly and restrictive with regard to how much data it's willing to
* eval(). In particular, the 10Mb disk image we use for the Windows 1.01 demo config fails in
* IE9 with an "Out of memory" exception. One work-around would be to chop the data into chunks
* (perhaps one track per chunk, using regular expressions) and then manually re-assemble it.
*
* However, it turns out that using JSON.parse(sDiskData) instead of eval("(" + sDiskData + ")")
* is a much easier fix. The only drawback is that we must first quote any unquoted property names
* and remove any comments, because while eval() was cool with them, JSON.parse() is more particular;
* the following RegExp replacements take care of those requirements.
*
* The use of hex values is something else that eval() was OK with, but JSON.parse() is not, and
* while I've stopped using hex values in DumpAPI responses (at least when "format=json" is specified),
* I can't guarantee they won't show up in "legacy" images, and there's no simple RegExp replacement
* for transforming hex values into decimal values, so I cop out and fall back to eval() if I detect
* any hex prefixes ("0x") in the sequence. Ditto for error messages, which appear like so:
*
* ["unrecognized disk path: test.img"]
*/
if (sData.indexOf("0x") < 0 && sData.substr(0, 2) != "[\"") {
data = JSON.parse(sData.replace(/([a-z]+):/gm, "\"$1\":").replace(/\/\/[^\n]*/gm, ""));
} else {
data = eval("(" + sData + ")");
}
resource.addrLoad = data['load'];
resource.addrExec = data['exec'];
if (a = data['bytes']) {
resource.aBytes = a;
}
else if (a = data['words']) {
/*
* Convert all words into bytes
*/
resource.aBytes = new Array(a.length * 2);
for (i = 0, ib = 0; i < a.length; i++) {
resource.aBytes[ib++] = a[i] & 0xff;
resource.aBytes[ib++] = (a[i] >> 8) & 0xff;
Component.assert(!(a[i] & ~0xffff));
}
}
else if (a = data['data']) {
/*
* Convert all dwords (longs) into bytes
*/
resource.aBytes = new Array(a.length * 4);
for (i = 0, ib = 0; i < a.length; i++) {
resource.aBytes[ib++] = a[i] & 0xff;
resource.aBytes[ib++] = (a[i] >> 8) & 0xff;
resource.aBytes[ib++] = (a[i] >> 16) & 0xff;
resource.aBytes[ib++] = (a[i] >> 24) & 0xff;
}
}
else {
resource.aBytes = data;
}
resource.aSymbols = data['symbols'];
if (!resource.aBytes.length) {
Component.error("Empty resource: " + sURL);
resource = null;
}
else if (resource.aBytes.length == 1) {
Component.error(resource.aBytes[0]);
resource = null;
}
} catch (e) {
Component.error("Resource data error (" + sURL + "): " + e.message);
resource = null;
}
}
else {
/*
* Parse the data manually; we assume it's a series of hex byte-values separated by whitespace.
*/
var ab = [];
var sHexData = sData.replace(/\n/gm, " ").replace(/ +$/, "");
var asHexData = sHexData.split(" ");
for (i = 0; i < asHexData.length; i++) {
var n = parseInt(asHexData[i], 16);
if (isNaN(n)) {
Component.error("Resource data error (" + sURL + "): invalid hex byte (" + asHexData[i] + ")");
break;
}
ab.push(n & 0xff);
}
if (i == asHexData.length) resource.aBytes = ab;
}
return resource;
}
/**
* 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, "/devices/pc/machine/5150/mda/64kb/machine.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)
*/
static sendReport(sApp, sVer, sURL, sUser, sType, sReport, sHostName)
{
var dataPost = {};
dataPost[ReportAPI.QUERY.APP] = sApp;
dataPost[ReportAPI.QUERY.VER] = sVer;
dataPost[ReportAPI.QUERY.URL] = sURL;
dataPost[ReportAPI.QUERY.USER] = sUser;
dataPost[ReportAPI.QUERY.TYPE] = sType;
dataPost[ReportAPI.QUERY.DATA] = sReport;
var sReportURL = (sHostName ? sHostName : "http://" + SITEHOST) + ReportAPI.ENDPOINT;
Web.getResource(sReportURL, dataPost, true);
}
/**
* getHost()
*
* @return {string}
*/
static getHost()
{
return ("http://" + (window ? window.location.host : SITEHOST));
}
/**
* getHostURL()
*
* @return {string|null}
*/
static getHostURL()
{
return (window ? window.location.href : null);
}
/**
* getHostProtocol()
*
* @return {string}
*/
static getHostProtocol()
{
return (window ? window.location.protocol : "file:");
}
/**
* getUserAgent()
*
* @return {string}
*/
static getUserAgent()
{
return (window ? window.navigator.userAgent : "");
}
/**
* alertUser(sMessage)
*
* @param {string} sMessage
*/
static alertUser(sMessage)
{
if (window) window.alert(sMessage); else Web.log(sMessage);
};
/**
* confirmUser(sPrompt)
*
* @param {string} sPrompt
* @returns {boolean} true if the user clicked OK, false if Cancel/Close
*/
static confirmUser(sPrompt)
{
var fResponse = false;
if (window) {
fResponse = window.confirm(sPrompt);
}
return fResponse;
}
/**
* promptUser()
*
* @param {string} sPrompt
* @param {string} [sDefault]
* @returns {string|null}
*/
static promptUser(sPrompt, sDefault)
{
var sResponse = null;
if (window) {
sResponse = window.prompt(sPrompt, sDefault === undefined ? "" : sDefault);
}
return sResponse;
}
/**
* hasLocalStorage
*
* true if localStorage support exists, is enabled, and works; false otherwise
*
* @return {boolean}
*/
static hasLocalStorage()
{
if (Web.fLocalStorage == null) {
var f = false;
if (window) {
try {
window.localStorage.setItem(Web.sLocalStorageTest, Web.sLocalStorageTest);
f = (window.localStorage.getItem(Web.sLocalStorageTest) == Web.sLocalStorageTest);
window.localStorage.removeItem(Web.sLocalStorageTest);
} catch (e) {
Web.logLocalStorageError(e);
f = false;
}
}
Web.fLocalStorage = f;
}
return Web.fLocalStorage;
}
/**
* logLocalStorageError(e)
*
* @param {Error} e is an exception
*/
static logLocalStorageError(e)
{
Web.log(e.message, "localStorage error");
}
/**
* 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
*/
static getLocalStorageItem(sKey)
{
var sValue;
if (window) {
try {
sValue = window.localStorage.getItem(sKey);
} catch (e) {
Web.logLocalStorageError(e);
}
}
return sValue;
}
/**
* setLocalStorageItem(sKey, sValue)
*
* @param {string} sKey
* @param {string} sValue
* @return {boolean} true if localStorage is available, false if not
*/
static setLocalStorageItem(sKey, sValue)
{
try {
window.localStorage.setItem(sKey, sValue);
return true;
} catch (e) {
Web.logLocalStorageError(e);
}
return false;
}
/**
* removeLocalStorageItem(sKey)
*
* @param {string} sKey
*/
static removeLocalStorageItem(sKey)
{
try {
window.localStorage.removeItem(sKey);
} catch (e) {
Web.logLocalStorageError(e);
}
}
/**
* getLocalStorageKeys()
*
* @return {Array}
*/
static getLocalStorageKeys()
{
var a = [];
try {
for (var i = 0, c = window.localStorage.length; i < c; i++) {
a.push(window.localStorage.key(i));
}
} catch (e) {
Web.logLocalStorageError(e);
}
return a;
}
/**
* reloadPage()
*/
static reloadPage()
{
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
*/
static isUserAgent(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 simple solution for now is to remove the call.
*
* Web.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);
}
return false;
}
/**
* isMobile()
*
* Check the browser's user-agent string for the substring "Mobi", as per Mozilla recommendation:
*
* https://developer.mozilla.org/en-US/docs/Browser_detection_using_the_user_agent
*
* @return {boolean} is true if the browser appears to be a mobile (ie, non-desktop) web browser, false if not
*/
static isMobile()
{
return Web.isUserAgent("Mobi");
}
/**
* getURLParameters(sParms)
*
* @param {string} [sParms] containing the parameter portion of a URL (ie, after the '?')
* @return {Object} containing properties for each parameter found
*/
static getURLParameters(sParms)
{
var aParms = {};
if (window) { // an alternative to "if (typeof module === 'undefined')" if require("defines") was used
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;
}
/**
* downloadFile(sData, sType, fBase64, sFileName)
*
* @param {string} sData
* @param {string} sType
* @param {boolean} [fBase64]
* @param {string} [sFileName]
*/
static downloadFile(sData, sType, fBase64, sFileName)
{
var link = null, sAlert;
var sURI = "data:application/" + sType + (fBase64 ? ";base64" : "") + ",";
if (!Web.isUserAgent("Firefox")) {
sURI += (fBase64 ? sData : encodeURI(sData));
} else {
sURI += (fBase64 ? sData : encodeURIComponent(sData));
}
if (sFileName) {
link = document.createElement('a');
if (typeof link.download != 'string') link = null;
}
if (link) {
link.href = sURI;
link.download = sFileName;
document.body.appendChild(link); // Firefox allegedly requires the link to be in the body
link.click();
document.body.removeChild(link);
sAlert = 'Check your Downloads folder for ' + sFileName + '.';
} else {
window.open(sURI);
sAlert = 'Check your browser for a new window/tab containing the requested data' + (sFileName ? (' (' + sFileName + ')') : '') + '.';
}
return sAlert;
}
/**
* onCountRepeat(n, fnRepeat, fnComplete, msDelay)
*
* Call fnRepeat() n times with an msDelay millisecond delay between calls,
* then call fnComplete() when n has been exhausted OR fnRepeat() returns false.
*
* @param {number} n
* @param {function()} fnRepeat
* @param {function()} fnComplete
* @param {number} [msDelay]
*/
static onCountRepeat(n, fnRepeat, fnComplete, msDelay)
{
var fnTimeout = function doCountRepeat()
{
n -= 1;
if (n >= 0) {
if (!fnRepeat()) n = 0;
}
if (n > 0) {
setTimeout(fnTimeout, msDelay || 0);
return;
}
fnComplete();
};
fnTimeout();
}
/**
* 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
*/
static onClickRepeat(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 ()
{
// Web.log("onMouseDown()");
if (!fIgnoreMouseEvents) {
if (!timer) {
ms = msDelay;
fnRepeat();
}
}
};
e.ontouchstart = function ()
{
// Web.log("onTouchStart()");
if (!timer) {
ms = msDelay;
fnRepeat();
}
};
e.onmouseup = e.onmouseout = function ()
{
// Web.log("onMouseUp()/onMouseOut()");
if (timer) {
clearTimeout(timer);
timer = null;
}
};
e.ontouchend = e.ontouchcancel = function ()
{
// Web.log("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;
};
}
/**
* onPageEvent(sName, fn)
*
* For 'onload', 'onunload', and 'onpageshow' events, most callers should NOT use this function, but
* instead use Web.onInit(), Web.onShow(), and Web.onExit(), respectively.
*
* The only components that should still use onPageEvent() are THIS component (see the bottom of this file)
* and components that need to capture other events (eg, the 'onresize' event in the Video component).
*
* This function creates a chain of callbacks, allowing multiple JavaScript modules to define handlers
* for the same event, which wouldn't be possible if everyone modified window['onload'], window['onunload'],
* etc, themselves. However, that's less of a concern now, because assuming everyone else is now using
* onInit(), onExit(), etc, then there really IS only one component setting the window callback: this one.
*
* NOTE: 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).
*
* @param {string} sFunc
* @param {function()} fn
*/
static onPageEvent(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)
*
* Use this instead of setting window.onload. Allows multiple JavaScript modules to define their own 'onload' event handler.
*
* @param {function()} fn
*/
static onInit(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.
*/
static onShow(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.
*/
static onExit(fn)
{
Web.aPageEventHandlers['exit'].push(fn);
};
/**
* doPageEvent(afn)
*
* @param {Array.<function()>} afn
*/
static doPageEvent(afn)
{
if (Web.fPageEventsEnabled) {
try {
for (var i = 0; i < afn.length; i++) {
afn[i]();
}
} catch (e) {
Web.notice("An unexpected exception occurred:\n\n" + e.message + "\n\nPlease send this information to support@pcjs.org. Thanks.");
}
}
};
/**
* enablePageEvents(fEnable)
*
* @param {boolean} fEnable is true to enable page events, false to disable (they're enabled by default)
*/
static enablePageEvents(fEnable)
{
if (!Web.fPageEventsEnabled && fEnable) {
Web.fPageEventsEnabled = true;
if (Web.fPageLoaded) Web.sendPageEvent('init');
if (Web.fPageShowed) Web.sendPageEvent('show');
return;
}
Web.fPageEventsEnabled = fEnable;
}
/**
* sendPageEvent(sEvent)
*
* This allows us to manually trigger page events.
*
* @param {string} sEvent (one of 'init', 'show' or 'exit')
*/
static sendPageEvent(sEvent)
{
if (Web.aPageEventHandlers[sEvent]) {
Web.doPageEvent(Web.aPageEventHandlers[sEvent]);
}
}
}
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.fPageLoaded = false; // set once the page's first 'onload' event has occurred
Web.fPageShowed = false; // set once the page's first 'onpageshow' event has occurred
Web.fPageEventsEnabled = true; // default is true, set to false (or true) by enablePageEvents()
/**
* fLocalStorage
*
* true if localStorage support exists, is enabled, and works; "falsey" otherwise
*
* @type {boolean|null}
*/
Web.fLocalStorage = null;
/**
* TODO: Is there any way to get the Closure Compiler to stop inlining this string? This isn't cutting it.
*
* @const {string}
*/
Web.sLocalStorageTest = "PCjs.localStorage";
Web.onPageEvent('onload', function onPageLoad() {
Web.fPageLoaded = true;
Web.doPageEvent(Web.aPageEventHandlers['init']);
});
Web.onPageEvent('onpageshow', function onPageShow() {
Web.fPageShowed = true;
Web.doPageEvent(Web.aPageEventHandlers['show']);
});
Web.onPageEvent(Web.isUserAgent("Opera") || Web.isUserAgent("iOS")? 'onunload' : 'onbeforeunload', function onPageUnload() {
Web.doPageEvent(Web.aPageEventHandlers['exit']);
});
export default Web;