v1.31.0 marks our first transition to ES6 classes, but only for PDPjs initially (which is why there must be both /lib and /es6 files in the shared folder for now)
This commit is contained in:
parent
dcdedb3513
commit
f4f6181923
999 changed files with 22351 additions and 211342 deletions
|
|
@ -301,7 +301,7 @@ function HTMLOut(sPath, sFile, fRebuild, req, done)
|
|||
this.done = done;
|
||||
|
||||
this.sHTML = "";
|
||||
this.sTemplate = null;
|
||||
this.sTemplate = "";
|
||||
this.aTokens = {};
|
||||
this.fRandomize = false;
|
||||
|
||||
|
|
@ -501,6 +501,26 @@ HTMLOut.filter = function(req, res, next)
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This is a hack to strip ES6 syntax from JavaScript files that is not (yet) supported by browsers;
|
||||
* specifically, "import" and "export" statements.
|
||||
*/
|
||||
if (sBaseExt == "js") {
|
||||
HTMLOut.logDebug("HTMLOut.filter(" + sBaseName + "): stripping unsupported ES6 syntax");
|
||||
fs.readFile(sPath, {encoding: "utf8"}, function doneReadFileJS(err, sData) {
|
||||
if (err) {
|
||||
HTMLOut.logError(err);
|
||||
next(); // alternatively: res.status(404).send("Cannot GET " + req.path);
|
||||
} else {
|
||||
sData = sData.replace(/^([ \t]*import\s+\S+\s+from\s+(['"]).*?\1;)/gm, "// $1");
|
||||
sData = sData.replace(/^([ \t]*export\s+default\s+\S+;)/gm, "// $1");
|
||||
res.set("Content-Type", "application/javascript");
|
||||
res.status(200).send(sData);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* The Safari "blank page" problem continues to plague us. Our first work-around was for directory
|
||||
* "index.html" documents, which we resolved by setting fSendDefault to true, so that we would always send
|
||||
|
|
@ -522,7 +542,7 @@ HTMLOut.filter = function(req, res, next)
|
|||
var sCacheControl = req.headers['cache-control'];
|
||||
if (sCacheControl && sCacheControl.indexOf("max-age=0") >= 0) {
|
||||
HTMLOut.logDebug("HTMLOut.filter(" + sBaseName + "): Safari work-around in progress");
|
||||
fs.readFile(sPath, {encoding: "utf8"}, function doneReadFileFilter(err, sData) {
|
||||
fs.readFile(sPath, {encoding: "utf8"}, function doneReadFileXML(err, sData) {
|
||||
if (err) {
|
||||
HTMLOut.logError(err);
|
||||
next(); // alternatively: res.status(404).send("Cannot GET " + req.path);
|
||||
|
|
@ -832,7 +852,7 @@ HTMLOut.prototype.findTokens = function(reTokens)
|
|||
* the token, each must be escaped.
|
||||
*/
|
||||
var sToken = token[0].substr(sIndent.length);
|
||||
sToken = sToken.replace(/([\(\)\*])/g, "\\$1");
|
||||
sToken = sToken.replace(/([()*])/g, "\\$1");
|
||||
|
||||
if (this.aTokens[sToken] === undefined) {
|
||||
this.aTokens[sToken] = "";
|
||||
|
|
@ -1635,6 +1655,7 @@ HTMLOut.prototype.getManifestXML = function(sToken, sIndent, aParms)
|
|||
sPageLink = matchCover.replace("/thumbs/", '/pages/').replace(/( ?)[0-9]*\.(jpeg|jpg)/, "$1" + match[1] + ".pdf");
|
||||
}
|
||||
}
|
||||
//noinspection JSCheckFunctionSignatures
|
||||
sPageName = '<a href="' + net.encodeURL(sPageLink, obj.req, obj.fDebug) + '" target="_blank">' + sPageName + '</a>';
|
||||
}
|
||||
sItemPages += sIndent + '\t\t\t<li>' + sPageName + '</li>\n';
|
||||
|
|
@ -1762,7 +1783,7 @@ HTMLOut.prototype.getMarkdownFile = function(sFile, sToken, sIndent, aParms, sPr
|
|||
var sTitle = match[2];
|
||||
match = sTitle.match(/<a [^>]*>([^<]*)<\/a>/);
|
||||
if (match) sTitle = match[1];
|
||||
obj.sHTML = obj.sHTML.replace(/(<title[^>]*>)([^\|]*)\|[^<]*(<\/title>)/, "$1$2| " + sTitle + "$3");
|
||||
obj.sHTML = obj.sHTML.replace(/(<title[^>]*>)([^|]*)\|[^<]*(<\/title>)/, "$1$2| " + sTitle + "$3");
|
||||
}
|
||||
/*
|
||||
* We need to query the MarkOut object for any machine definitions that the current Markdown
|
||||
|
|
@ -1881,7 +1902,7 @@ HTMLOut.prototype.processMachines = function(aMachines, buildOptions, done)
|
|||
|
||||
var sScriptEmbed = "";
|
||||
if (infoMachine['func']) {
|
||||
sScriptEmbed = '<script type="text/javascript">' + 'window.' + infoMachine['func'];
|
||||
sScriptEmbed = '<' + 'script type="text/javascript">window.' + infoMachine['func'];
|
||||
sScriptEmbed += "('" + infoMachine['id'] + "','" + infoMachine['xml'] + "'";
|
||||
sScriptEmbed += (infoMachine['xsl']? (",'" + infoMachine['xsl'] + "'") : ",''");
|
||||
sScriptEmbed += (infoMachine['parms']? (",'" + infoMachine['parms'] + "'") : '') + ');</script>';
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -32,9 +32,7 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
if (NODE) {
|
||||
var PDP11 = require("./defines");
|
||||
}
|
||||
import PDP11 from "./defines";
|
||||
|
||||
/*
|
||||
* Decoding starts at the bottom of this file, in op1120() and op1145().
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -883,13 +883,4 @@ PDP11.ACCESS.UPDATE_BYTE = PDP11.ACCESS.BYTE | PDP11.ACCESS.UPDATE; // forme
|
|||
*/
|
||||
PDP11.PSW.FLAGS = (PDP11.PSW.NF | PDP11.PSW.ZF | PDP11.PSW.VF | PDP11.PSW.CF);
|
||||
|
||||
if (NODE) {
|
||||
global.APPCLASS = APPCLASS;
|
||||
global.APPNAME = APPNAME;
|
||||
global.DEBUGGER = DEBUGGER;
|
||||
global.BYTEARRAYS = BYTEARRAYS;
|
||||
global.TYPEDARRAYS = TYPEDARRAYS;
|
||||
global.PDP11 = PDP11;
|
||||
|
||||
module.exports = PDP11;
|
||||
}
|
||||
export default PDP11;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -32,86 +32,80 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
if (NODE) {
|
||||
var str = require("../../shared/lib/strlib");
|
||||
var web = require("../../shared/lib/weblib");
|
||||
var Component = require("../../shared/lib/component");
|
||||
var Keys = require("../../shared/lib/keys");
|
||||
var PDP11 = require("./defines");
|
||||
var MessagesPDP11 = require("./messages");
|
||||
import Web from "../../shared/es6/weblib";
|
||||
import Component from "../../shared/es6/component";
|
||||
import PDP11 from "./defines";
|
||||
import MessagesPDP11 from "./messages";
|
||||
|
||||
class KeyboardPDP11 extends Component {
|
||||
/**
|
||||
* KeyboardPDP11(parmsKbd)
|
||||
*
|
||||
* @param {Object} parmsKbd
|
||||
*/
|
||||
constructor(parmsKbd)
|
||||
{
|
||||
super("Keyboard", parmsKbd, KeyboardPDP11, MessagesPDP11.KEYBOARD);
|
||||
|
||||
this.setReady();
|
||||
}
|
||||
|
||||
/**
|
||||
* setBinding(sType, sBinding, control, sValue)
|
||||
*
|
||||
* @this {KeyboardPDP11}
|
||||
* @param {string|null} sType is the type of the HTML control (eg, "button", "textarea", "register", "flag", "rled", etc)
|
||||
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "esc")
|
||||
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
|
||||
* @param {string} [sValue] optional data value
|
||||
* @return {boolean} true if binding was successful, false if unrecognized binding request
|
||||
*/
|
||||
setBinding(sType, sBinding, control, sValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* initBus(cmp, bus, cpu, dbg)
|
||||
*
|
||||
* @this {KeyboardPDP11}
|
||||
* @param {ComputerPDP11} cmp
|
||||
* @param {BusPDP11} bus
|
||||
* @param {CPUStatePDP11} cpu
|
||||
* @param {DebuggerPDP11} dbg
|
||||
*/
|
||||
initBus(cmp, bus, cpu, dbg)
|
||||
{
|
||||
this.cmp = cmp;
|
||||
this.cpu = cpu;
|
||||
this.dbg = dbg; // NOTE: The "dbg" property must be set for the message functions to work
|
||||
}
|
||||
|
||||
/**
|
||||
* KeyboardPDP11.init()
|
||||
*
|
||||
* This function operates on every HTML element of class "keyboard", extracting the
|
||||
* JSON-encoded parameters for the Keyboard constructor from the element's "data-value"
|
||||
* attribute, invoking the constructor to create a Keyboard component, and then binding
|
||||
* any associated HTML controls to the new component.
|
||||
*/
|
||||
static init()
|
||||
{
|
||||
var aeKbd = Component.getElementsByClass(document, PDP11.APPCLASS, "keyboard");
|
||||
for (var iKbd = 0; iKbd < aeKbd.length; iKbd++) {
|
||||
var eKbd = aeKbd[iKbd];
|
||||
var parmsKbd = Component.getComponentParms(eKbd);
|
||||
var kbd = new KeyboardPDP11(parmsKbd);
|
||||
Component.bindComponentControls(kbd, eKbd, PDP11.APPCLASS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* KeyboardPDP11(parmsKbd)
|
||||
*
|
||||
* @constructor
|
||||
* @extends Component
|
||||
* @param {Object} parmsKbd
|
||||
*/
|
||||
function KeyboardPDP11(parmsKbd)
|
||||
{
|
||||
Component.call(this, "Keyboard", parmsKbd, KeyboardPDP11, MessagesPDP11.KEYBOARD);
|
||||
|
||||
this.setReady();
|
||||
}
|
||||
|
||||
Component.subclass(KeyboardPDP11);
|
||||
|
||||
KeyboardPDP11.MINPRESSTIME = 100; // 100ms
|
||||
|
||||
/**
|
||||
* setBinding(sType, sBinding, control, sValue)
|
||||
*
|
||||
* @this {KeyboardPDP11}
|
||||
* @param {string|null} sType is the type of the HTML control (eg, "button", "textarea", "register", "flag", "rled", etc)
|
||||
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "esc")
|
||||
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
|
||||
* @param {string} [sValue] optional data value
|
||||
* @return {boolean} true if binding was successful, false if unrecognized binding request
|
||||
*/
|
||||
KeyboardPDP11.prototype.setBinding = function(sType, sBinding, control, sValue)
|
||||
{
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* initBus(cmp, bus, cpu, dbg)
|
||||
*
|
||||
* @this {KeyboardPDP11}
|
||||
* @param {ComputerPDP11} cmp
|
||||
* @param {BusPDP11} bus
|
||||
* @param {CPUStatePDP11} cpu
|
||||
* @param {DebuggerPDP11} dbg
|
||||
*/
|
||||
KeyboardPDP11.prototype.initBus = function(cmp, bus, cpu, dbg)
|
||||
{
|
||||
this.cmp = cmp;
|
||||
this.cpu = cpu;
|
||||
this.dbg = dbg; // NOTE: The "dbg" property must be set for the message functions to work
|
||||
};
|
||||
|
||||
/**
|
||||
* KeyboardPDP11.init()
|
||||
*
|
||||
* This function operates on every HTML element of class "keyboard", extracting the
|
||||
* JSON-encoded parameters for the Keyboard constructor from the element's "data-value"
|
||||
* attribute, invoking the constructor to create a Keyboard component, and then binding
|
||||
* any associated HTML controls to the new component.
|
||||
*/
|
||||
KeyboardPDP11.init = function()
|
||||
{
|
||||
var aeKbd = Component.getElementsByClass(document, PDP11.APPCLASS, "keyboard");
|
||||
for (var iKbd = 0; iKbd < aeKbd.length; iKbd++) {
|
||||
var eKbd = aeKbd[iKbd];
|
||||
var parmsKbd = Component.getComponentParms(eKbd);
|
||||
var kbd = new KeyboardPDP11(parmsKbd);
|
||||
Component.bindComponentControls(kbd, eKbd, PDP11.APPCLASS);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Initialize every Keyboard module on the page.
|
||||
*/
|
||||
web.onInit(KeyboardPDP11.init);
|
||||
Web.onInit(KeyboardPDP11.init);
|
||||
|
||||
if (NODE) module.exports = KeyboardPDP11;
|
||||
export default KeyboardPDP11;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -117,4 +117,4 @@ MessagesPDP11.CATEGORIES = {
|
|||
"halt": MessagesPDP11.HALT
|
||||
};
|
||||
|
||||
if (NODE) module.exports = MessagesPDP11;
|
||||
export default MessagesPDP11;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -32,383 +32,378 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
if (NODE) {
|
||||
var str = require("../../shared/lib/strlib");
|
||||
var web = require("../../shared/lib/weblib");
|
||||
var DumpAPI = require("../../shared/lib/dumpapi");
|
||||
var Component = require("../../shared/lib/component");
|
||||
var State = require("../../shared/lib/state");
|
||||
var PDP11 = require("./defines");
|
||||
var MemoryPDP11 = require("./memory");
|
||||
var MessagesPDP11 = require("./messages");
|
||||
}
|
||||
import Str from "../../shared/lib/strlib";
|
||||
import Web from "../../shared/lib/weblib";
|
||||
import DumpAPI from "../../shared/lib/dumpapi";
|
||||
import Component from "../../shared/lib/component";
|
||||
import PDP11 from "./defines";
|
||||
import MemoryPDP11 from "./memory";
|
||||
import MessagesPDP11 from "./messages";
|
||||
|
||||
/**
|
||||
* RAMPDP11(parmsRAM)
|
||||
*
|
||||
* The RAMPDP11 component expects the following (parmsRAM) properties:
|
||||
*
|
||||
* addr: starting physical address of RAM (default is 0)
|
||||
* size: amount of RAM, in bytes (default is 0, which means defer to motherboard switch settings)
|
||||
* file: name of optional data file to load into RAM (default is "")
|
||||
* load: optional file load address (overrides any load address specified in the data file; default is null)
|
||||
* exec: optional file exec address (overrides any exec address specified in the data file; default is null)
|
||||
*
|
||||
* NOTE: We make a note of the specified size, but no memory is initially allocated for the RAM until the
|
||||
* Computer component calls powerUp().
|
||||
*
|
||||
* TODO: I seem to recall a PDP-11 diagnostic that failed if total RAM wasn't a multiple of 16Kb; our Bus
|
||||
* component defaults to a block size that matches BusPDP11.IOPAGE_LENGTH (ie, 8Kb), and we even allow partial
|
||||
* block allocations, so internally, we don't have that requirement, but for better compatibility, perhaps we
|
||||
* should display a non-fatal warning if addr or size don't fall on 16Kb boundaries.
|
||||
*
|
||||
* @constructor
|
||||
* @extends Component
|
||||
* @param {Object} parmsRAM
|
||||
*/
|
||||
function RAMPDP11(parmsRAM)
|
||||
{
|
||||
Component.call(this, "RAM", parmsRAM, RAMPDP11);
|
||||
class RAMPDP11 extends Component {
|
||||
/**
|
||||
* RAMPDP11(parmsRAM)
|
||||
*
|
||||
* The RAMPDP11 component expects the following (parmsRAM) properties:
|
||||
*
|
||||
* addr: starting physical address of RAM (default is 0)
|
||||
* size: amount of RAM, in bytes (default is 0, which means defer to motherboard switch settings)
|
||||
* file: name of optional data file to load into RAM (default is "")
|
||||
* load: optional file load address (overrides any load address specified in the data file; default is null)
|
||||
* exec: optional file exec address (overrides any exec address specified in the data file; default is null)
|
||||
*
|
||||
* NOTE: We make a note of the specified size, but no memory is initially allocated for the RAM until the
|
||||
* Computer component calls powerUp().
|
||||
*
|
||||
* TODO: I seem to recall a PDP-11 diagnostic that failed if total RAM wasn't a multiple of 16Kb; our Bus
|
||||
* component defaults to a block size that matches BusPDP11.IOPAGE_LENGTH (ie, 8Kb), and we even allow partial
|
||||
* block allocations, so internally, we don't have that requirement, but for better compatibility, perhaps we
|
||||
* should display a non-fatal warning if addr or size don't fall on 16Kb boundaries.
|
||||
*
|
||||
* @param {Object} parmsRAM
|
||||
*/
|
||||
constructor(parmsRAM)
|
||||
{
|
||||
super("RAM", parmsRAM, RAMPDP11);
|
||||
|
||||
this.abInit = null;
|
||||
this.aSymbols = null;
|
||||
this.abInit = null;
|
||||
this.aSymbols = null;
|
||||
|
||||
this.addrRAM = parmsRAM['addr'];
|
||||
this.sizeRAM = parmsRAM['size'];
|
||||
this.addrLoad = parmsRAM['load'];
|
||||
this.addrExec = parmsRAM['exec'];
|
||||
this.addrRAM = parmsRAM['addr'];
|
||||
this.sizeRAM = parmsRAM['size'];
|
||||
this.addrLoad = parmsRAM['load'];
|
||||
this.addrExec = parmsRAM['exec'];
|
||||
|
||||
this.fInstalled = (!!this.sizeRAM); // 0 is the default value for 'size' when none is specified
|
||||
this.fAllocated = false;
|
||||
this.fInstalled = (!!this.sizeRAM); // 0 is the default value for 'size' when none is specified
|
||||
this.fAllocated = false;
|
||||
|
||||
this.sFilePath = parmsRAM['file'];
|
||||
this.sFileName = str.getBaseName(this.sFilePath);
|
||||
this.sFilePath = parmsRAM['file'];
|
||||
this.sFileName = Str.getBaseName(this.sFilePath);
|
||||
|
||||
if (this.sFilePath) {
|
||||
var sFileURL = this.sFilePath;
|
||||
if (DEBUG) this.log('load("' + sFileURL + '")');
|
||||
/*
|
||||
* If the selected data file has a ".json" extension, then we assume it's pre-converted
|
||||
* JSON-encoded data, so we load it as-is; ditto for ROM files with a ".hex" extension.
|
||||
* Otherwise, we ask our server-side converter to return the file in a JSON-compatible format.
|
||||
*/
|
||||
var sFileExt = str.getExtension(this.sFileName);
|
||||
if (sFileExt != DumpAPI.FORMAT.JSON && sFileExt != DumpAPI.FORMAT.HEX) {
|
||||
sFileURL = web.getHost() + DumpAPI.ENDPOINT + '?' + DumpAPI.QUERY.FILE + '=' + this.sFilePath + '&' + DumpAPI.QUERY.FORMAT + '=' + DumpAPI.FORMAT.BYTES + '&' + DumpAPI.QUERY.DECIMAL + '=true';
|
||||
if (this.sFilePath) {
|
||||
var sFileURL = this.sFilePath;
|
||||
if (DEBUG) this.log('load("' + sFileURL + '")');
|
||||
/*
|
||||
* If the selected data file has a ".json" extension, then we assume it's pre-converted
|
||||
* JSON-encoded data, so we load it as-is; ditto for ROM files with a ".hex" extension.
|
||||
* Otherwise, we ask our server-side converter to return the file in a JSON-compatible format.
|
||||
*/
|
||||
var sFileExt = Str.getExtension(this.sFileName);
|
||||
if (sFileExt != DumpAPI.FORMAT.JSON && sFileExt != DumpAPI.FORMAT.HEX) {
|
||||
sFileURL = Web.getHost() + DumpAPI.ENDPOINT + '?' + DumpAPI.QUERY.FILE + '=' + this.sFilePath + '&' + DumpAPI.QUERY.FORMAT + '=' + DumpAPI.FORMAT.BYTES + '&' + DumpAPI.QUERY.DECIMAL + '=true';
|
||||
}
|
||||
var ram = this;
|
||||
Web.getResource(sFileURL, null, true, function doneLoad(sURL, sResponse, nErrorCode) {
|
||||
ram.finishLoad(sURL, sResponse, nErrorCode);
|
||||
});
|
||||
}
|
||||
var ram = this;
|
||||
web.getResource(sFileURL, null, true, function doneLoad(sURL, sResponse, nErrorCode) {
|
||||
ram.finishLoad(sURL, sResponse, nErrorCode);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Component.subclass(RAMPDP11);
|
||||
/**
|
||||
* initBus(cmp, bus, cpu, dbg)
|
||||
*
|
||||
* @this {RAMPDP11}
|
||||
* @param {ComputerPDP11} cmp
|
||||
* @param {BusPDP11} bus
|
||||
* @param {CPUStatePDP11} cpu
|
||||
* @param {DebuggerPDP11} dbg
|
||||
*/
|
||||
initBus(cmp, bus, cpu, dbg)
|
||||
{
|
||||
this.bus = bus;
|
||||
this.cpu = cpu;
|
||||
this.dbg = dbg;
|
||||
this.initRAM();
|
||||
}
|
||||
|
||||
/**
|
||||
* initBus(cmp, bus, cpu, dbg)
|
||||
*
|
||||
* @this {RAMPDP11}
|
||||
* @param {ComputerPDP11} cmp
|
||||
* @param {BusPDP11} bus
|
||||
* @param {CPUStatePDP11} cpu
|
||||
* @param {DebuggerPDP11} dbg
|
||||
*/
|
||||
RAMPDP11.prototype.initBus = function(cmp, bus, cpu, dbg)
|
||||
{
|
||||
this.bus = bus;
|
||||
this.cpu = cpu;
|
||||
this.dbg = dbg;
|
||||
this.initRAM();
|
||||
};
|
||||
|
||||
/**
|
||||
* powerUp(data, fRepower)
|
||||
*
|
||||
* @this {RAMPDP11}
|
||||
* @param {Object|null} data
|
||||
* @param {boolean} [fRepower]
|
||||
* @return {boolean} true if successful, false if failure
|
||||
*/
|
||||
RAMPDP11.prototype.powerUp = function(data, fRepower)
|
||||
{
|
||||
if (this.aSymbols) {
|
||||
if (this.dbg) {
|
||||
this.dbg.addSymbols(this.id, this.addrRAM, this.sizeRAM, this.aSymbols);
|
||||
/**
|
||||
* powerUp(data, fRepower)
|
||||
*
|
||||
* @this {RAMPDP11}
|
||||
* @param {Object|null} data
|
||||
* @param {boolean} [fRepower]
|
||||
* @return {boolean} true if successful, false if failure
|
||||
*/
|
||||
powerUp(data, fRepower)
|
||||
{
|
||||
if (this.aSymbols) {
|
||||
if (this.dbg) {
|
||||
this.dbg.addSymbols(this.id, this.addrRAM, this.sizeRAM, this.aSymbols);
|
||||
}
|
||||
/*
|
||||
* Our only role in the handling of symbols is to hand them off to the Debugger at our
|
||||
* first opportunity. Now that we've done that, our copy of the symbols, if any, are toast.
|
||||
*/
|
||||
delete this.aSymbols;
|
||||
}
|
||||
/*
|
||||
* Our only role in the handling of symbols is to hand them off to the Debugger at our
|
||||
* first opportunity. Now that we've done that, our copy of the symbols, if any, are toast.
|
||||
* The Computer powers up the CPU last, at which point CPUState state is restored,
|
||||
* which includes the Bus state, and since we use the Bus to allocate all our memory,
|
||||
* memory contents are already restored for us, so we don't need the usual restore
|
||||
* logic.
|
||||
*/
|
||||
delete this.aSymbols;
|
||||
return true;
|
||||
}
|
||||
/*
|
||||
* The Computer powers up the CPU last, at which point CPUState state is restored,
|
||||
* which includes the Bus state, and since we use the Bus to allocate all our memory,
|
||||
* memory contents are already restored for us, so we don't need the usual restore
|
||||
* logic.
|
||||
*/
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* powerDown(fSave, fShutdown)
|
||||
*
|
||||
* @this {RAMPDP11}
|
||||
* @param {boolean} [fSave]
|
||||
* @param {boolean} [fShutdown]
|
||||
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
|
||||
*/
|
||||
RAMPDP11.prototype.powerDown = function(fSave, fShutdown)
|
||||
{
|
||||
/*
|
||||
* The Computer powers down the CPU first, at which point CPUState state is saved,
|
||||
* which includes the Bus state, and since we use the Bus component to allocate all
|
||||
* our memory, memory contents are already saved for us, so we don't need the usual
|
||||
* save logic.
|
||||
/**
|
||||
* powerDown(fSave, fShutdown)
|
||||
*
|
||||
* @this {RAMPDP11}
|
||||
* @param {boolean} [fSave]
|
||||
* @param {boolean} [fShutdown]
|
||||
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
|
||||
*/
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* finishLoad(sURL, sData, nErrorCode)
|
||||
*
|
||||
* @this {RAMPDP11}
|
||||
* @param {string} sURL
|
||||
* @param {string} sData
|
||||
* @param {number} nErrorCode (response from server if anything other than 200)
|
||||
*/
|
||||
RAMPDP11.prototype.finishLoad = function(sURL, sData, nErrorCode)
|
||||
{
|
||||
if (nErrorCode) {
|
||||
this.notice("Unable to load RAM resource (error " + nErrorCode + ": " + sURL + ")");
|
||||
this.sFilePath = null;
|
||||
powerDown(fSave, fShutdown)
|
||||
{
|
||||
/*
|
||||
* The Computer powers down the CPU first, at which point CPUState state is saved,
|
||||
* which includes the Bus state, and since we use the Bus component to allocate all
|
||||
* our memory, memory contents are already saved for us, so we don't need the usual
|
||||
* save logic.
|
||||
*/
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
Component.addMachineResource(this.idMachine, sURL, sData);
|
||||
var resource = web.parseMemoryResource(sURL, sData);
|
||||
if (resource) {
|
||||
this.abInit = resource.aBytes;
|
||||
this.aSymbols = resource.aSymbols;
|
||||
if (this.addrLoad == null) this.addrLoad = resource.addrLoad;
|
||||
if (this.addrExec == null) this.addrExec = resource.addrExec;
|
||||
} else {
|
||||
|
||||
/**
|
||||
* finishLoad(sURL, sData, nErrorCode)
|
||||
*
|
||||
* @this {RAMPDP11}
|
||||
* @param {string} sURL
|
||||
* @param {string} sData
|
||||
* @param {number} nErrorCode (response from server if anything other than 200)
|
||||
*/
|
||||
finishLoad(sURL, sData, nErrorCode)
|
||||
{
|
||||
if (nErrorCode) {
|
||||
this.notice("Unable to load RAM resource (error " + nErrorCode + ": " + sURL + ")");
|
||||
this.sFilePath = null;
|
||||
}
|
||||
}
|
||||
this.initRAM();
|
||||
};
|
||||
|
||||
/**
|
||||
* initRAM()
|
||||
*
|
||||
* This function is called by both initBus() and finishLoad(), but it cannot copy the initial data into place
|
||||
* until after initBus() has received the Bus component AND finishLoad() has received the data. When both those
|
||||
* criteria are satisfied, the component becomes "ready".
|
||||
*
|
||||
* @this {RAMPDP11}
|
||||
*/
|
||||
RAMPDP11.prototype.initRAM = function()
|
||||
{
|
||||
if (!this.bus) return;
|
||||
|
||||
if (!this.fAllocated && this.sizeRAM) {
|
||||
if (this.bus.addMemory(this.addrRAM, this.sizeRAM, MemoryPDP11.TYPE.RAM)) {
|
||||
this.fAllocated = true;
|
||||
} else {
|
||||
this.sizeRAM = 0; // don't bother trying again (it just results in redundant error messages)
|
||||
}
|
||||
}
|
||||
if (!this.isReady()) {
|
||||
if (!this.fAllocated) {
|
||||
Component.error("No RAM allocated");
|
||||
}
|
||||
else if (this.sFilePath) {
|
||||
/*
|
||||
* Too early...
|
||||
*/
|
||||
if (!this.abInit || !this.bus) return;
|
||||
this.loadImage(this.abInit, this.addrLoad, this.addrExec, this.addrRAM);
|
||||
/*
|
||||
* NOTE: We now retain this data, so that reset() can return the RAM to its predefined state.
|
||||
*
|
||||
* delete this.abInit;
|
||||
*/
|
||||
}
|
||||
this.setReady();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* reset()
|
||||
*
|
||||
* @this {RAMPDP11}
|
||||
*/
|
||||
RAMPDP11.prototype.reset = function()
|
||||
{
|
||||
if (this.fAllocated) {
|
||||
/*
|
||||
* TODO: Add a configuration parameter for selecting the byte pattern on reset?
|
||||
* Note that when memory blocks are originally created, they are currently always
|
||||
* zero-initialized, so this would only affect resets.
|
||||
*/
|
||||
this.bus.zeroMemory(this.addrRAM, this.sizeRAM, 0);
|
||||
if (this.abInit) {
|
||||
this.loadImage(this.abInit, this.addrLoad, this.addrExec, this.addrRAM, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* loadImage(aBytes, addrLoad, addrExec, addrInit, fStart)
|
||||
*
|
||||
* If the array contains a PAPER tape image in the "Absolute Format," load it as specified
|
||||
* by the format; otherwise, load it as-is using the address(es) supplied.
|
||||
*
|
||||
* @this {RAMPDP11}
|
||||
* @param {Array|Uint8Array} aBytes
|
||||
* @param {number|null} [addrLoad]
|
||||
* @param {number|null} [addrExec] (this CAN override any starting address INSIDE the image)
|
||||
* @param {number|null} [addrInit]
|
||||
* @param {boolean} [fStart]
|
||||
* @return {boolean} (true if loaded, false if not)
|
||||
*/
|
||||
RAMPDP11.prototype.loadImage = function(aBytes, addrLoad, addrExec, addrInit, fStart)
|
||||
{
|
||||
var fStop = false;
|
||||
var fLoaded = false;
|
||||
/*
|
||||
* Data on tapes in the "Absolute Format" is organized into blocks; each block begins with
|
||||
* a 6-byte header:
|
||||
*
|
||||
* 2-byte signature (0x0001)
|
||||
* 2-byte block length (N + 6, because it includes the 6-byte header)
|
||||
* 2-byte load address
|
||||
*
|
||||
* followed by N data bytes. If N is zero, then the 2-byte load address is the exec address,
|
||||
* unless the address is odd (usually 1). DEC's Absolute Loader jumps to the exec address
|
||||
* in former case, halts in the latter.
|
||||
*
|
||||
* All values are stored "little endian" (low byte followed by high byte), just like the
|
||||
* PDP-11's memory architecture.
|
||||
*
|
||||
* After the data bytes, there is a single checksum byte. The 8-bit sum of all the bytes in
|
||||
* the block (including the header bytes and checksum byte) should be zero.
|
||||
*
|
||||
* ANOMALIES: Tape files don't always begin with a signature word, so I allow any number of
|
||||
* leading zeros before the first signature. Tape files don't always end cleanly either, so as
|
||||
* soon as I see an invalid signature, I break out of the loop without signalling an error, as
|
||||
* long as at least ONE block was successfully processed. In fact, it's possible that as
|
||||
* soon as a block with ZERO data bytes is encountered, processing is supposed to stop, but
|
||||
* I haven't examined enough tapes (or the Absolute Loader code) to know for sure.
|
||||
*/
|
||||
if (addrLoad == null) {
|
||||
var off = 0, fError = false;
|
||||
while (off < aBytes.length - 1) {
|
||||
var w = (aBytes[off] & 0xff) | ((aBytes[off+1] & 0xff) << 8);
|
||||
if (!w) { // ignore pairs of leading zeros
|
||||
off += 2;
|
||||
continue;
|
||||
}
|
||||
if (!(w & 0xff)) { // as well as single bytes of zero
|
||||
off++;
|
||||
continue;
|
||||
}
|
||||
var offBlock = off;
|
||||
if (w != 0x0001) {
|
||||
this.printMessage("invalid signature (" + str.toHexWord(w) + ") at offset " + str.toHexWord(offBlock), MessagesPDP11.PAPER);
|
||||
break;
|
||||
}
|
||||
if (off + 6 >= aBytes.length) {
|
||||
this.printMessage("invalid block at offset " + str.toHexWord(offBlock), MessagesPDP11.PAPER);
|
||||
break;
|
||||
}
|
||||
off += 2;
|
||||
var checksum = w;
|
||||
var len = (aBytes[off++] & 0xff) | ((aBytes[off++] & 0xff) << 8);
|
||||
var addr = (aBytes[off++] & 0xff) | ((aBytes[off++] & 0xff) << 8);
|
||||
checksum += (len & 0xff) + (len >> 8) + (addr & 0xff) + (addr >> 8);
|
||||
var offData = off, cbData = len -= 6;
|
||||
while (len > 0 && off < aBytes.length) {
|
||||
checksum += aBytes[off++] & 0xff;
|
||||
len--;
|
||||
}
|
||||
if (len != 0 || off >= aBytes.length) {
|
||||
this.printMessage("insufficient data for block at offset " + str.toHexWord(offBlock), MessagesPDP11.PAPER);
|
||||
break;
|
||||
}
|
||||
checksum += aBytes[off++] & 0xff;
|
||||
if (checksum & 0xff) {
|
||||
this.printMessage("invalid checksum (" + str.toHexByte(checksum) + ") for block at offset " + str.toHexWord(offBlock), MessagesPDP11.PAPER);
|
||||
break;
|
||||
}
|
||||
if (!cbData) {
|
||||
if (addr & 0x1) {
|
||||
fStop = true;
|
||||
} else {
|
||||
if (addrExec == null) addrExec = addr;
|
||||
}
|
||||
if (addrExec != null) this.printMessage("starting address: " + str.toHexWord(addrExec), MessagesPDP11.PAPER);
|
||||
else {
|
||||
Component.addMachineResource(this.idMachine, sURL, sData);
|
||||
var resource = Web.parseMemoryResource(sURL, sData);
|
||||
if (resource) {
|
||||
this.abInit = resource.aBytes;
|
||||
this.aSymbols = resource.aSymbols;
|
||||
if (this.addrLoad == null) this.addrLoad = resource.addrLoad;
|
||||
if (this.addrExec == null) this.addrExec = resource.addrExec;
|
||||
} else {
|
||||
this.printMessage("loading " + str.toHexWord(cbData) + " bytes at " + str.toHexWord(addr) + "-" + str.toHexWord(addr + cbData), MessagesPDP11.PAPER);
|
||||
while (cbData--) {
|
||||
this.bus.setByteDirect(addr++, aBytes[offData++] & 0xff);
|
||||
}
|
||||
this.sFilePath = null;
|
||||
}
|
||||
fLoaded = true;
|
||||
}
|
||||
this.initRAM();
|
||||
}
|
||||
if (!fLoaded) {
|
||||
if (addrLoad == null) addrLoad = addrInit;
|
||||
if (addrLoad != null) {
|
||||
for (var i = 0; i < aBytes.length; i++) {
|
||||
this.bus.setByteDirect(addrLoad + i, aBytes[i]);
|
||||
}
|
||||
fLoaded = true;
|
||||
}
|
||||
}
|
||||
if (fLoaded) {
|
||||
/*
|
||||
* Set the start address to whatever the caller provided, or failing that, whatever start
|
||||
* address was specified inside the image.
|
||||
*
|
||||
* For example, the diagnostic "MAINDEC-11-D0AA-PB" doesn't include a start address inside the
|
||||
* image, but we know that the directions for that diagnostic say to "Start and Restart at 200",
|
||||
* so we have manually inserted an "exec":128 in the JSON containing the image.
|
||||
*/
|
||||
if (addrExec == null || fStop) {
|
||||
this.cpu.stopCPU();
|
||||
fStart = false;
|
||||
}
|
||||
if (addrExec != null) {
|
||||
this.cpu.setReset(addrExec, fStart);
|
||||
}
|
||||
}
|
||||
return fLoaded;
|
||||
};
|
||||
|
||||
/**
|
||||
* RAMPDP11.init()
|
||||
*
|
||||
* This function operates on every HTML element of class "ram", extracting the
|
||||
* JSON-encoded parameters for the RAMPDP11 constructor from the element's "data-value"
|
||||
* attribute, invoking the constructor to create a RAMPDP11 component, and then binding
|
||||
* any associated HTML controls to the new component.
|
||||
*/
|
||||
RAMPDP11.init = function()
|
||||
{
|
||||
var aeRAM = Component.getElementsByClass(document, PDP11.APPCLASS, "ram");
|
||||
for (var iRAM = 0; iRAM < aeRAM.length; iRAM++) {
|
||||
var eRAM = aeRAM[iRAM];
|
||||
var parmsRAM = Component.getComponentParms(eRAM);
|
||||
var ram = new RAMPDP11(parmsRAM);
|
||||
Component.bindComponentControls(ram, eRAM, PDP11.APPCLASS);
|
||||
/**
|
||||
* initRAM()
|
||||
*
|
||||
* This function is called by both initBus() and finishLoad(), but it cannot copy the initial data into place
|
||||
* until after initBus() has received the Bus component AND finishLoad() has received the data. When both those
|
||||
* criteria are satisfied, the component becomes "ready".
|
||||
*
|
||||
* @this {RAMPDP11}
|
||||
*/
|
||||
initRAM()
|
||||
{
|
||||
if (!this.bus) return;
|
||||
|
||||
if (!this.fAllocated && this.sizeRAM) {
|
||||
if (this.bus.addMemory(this.addrRAM, this.sizeRAM, MemoryPDP11.TYPE.RAM)) {
|
||||
this.fAllocated = true;
|
||||
} else {
|
||||
this.sizeRAM = 0; // don't bother trying again (it just results in redundant error messages)
|
||||
}
|
||||
}
|
||||
if (!this.isReady()) {
|
||||
if (!this.fAllocated) {
|
||||
Component.error("No RAM allocated");
|
||||
}
|
||||
else if (this.sFilePath) {
|
||||
/*
|
||||
* Too early...
|
||||
*/
|
||||
if (!this.abInit || !this.bus) return;
|
||||
this.loadImage(this.abInit, this.addrLoad, this.addrExec, this.addrRAM);
|
||||
/*
|
||||
* NOTE: We now retain this data, so that reset() can return the RAM to its predefined state.
|
||||
*
|
||||
* delete this.abInit;
|
||||
*/
|
||||
}
|
||||
this.setReady();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* reset()
|
||||
*
|
||||
* @this {RAMPDP11}
|
||||
*/
|
||||
reset()
|
||||
{
|
||||
if (this.fAllocated) {
|
||||
/*
|
||||
* TODO: Add a configuration parameter for selecting the byte pattern on reset?
|
||||
* Note that when memory blocks are originally created, they are currently always
|
||||
* zero-initialized, so this would only affect resets.
|
||||
*/
|
||||
this.bus.zeroMemory(this.addrRAM, this.sizeRAM, 0);
|
||||
if (this.abInit) {
|
||||
this.loadImage(this.abInit, this.addrLoad, this.addrExec, this.addrRAM, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* loadImage(aBytes, addrLoad, addrExec, addrInit, fStart)
|
||||
*
|
||||
* If the array contains a PAPER tape image in the "Absolute Format," load it as specified
|
||||
* by the format; otherwise, load it as-is using the address(es) supplied.
|
||||
*
|
||||
* @this {RAMPDP11}
|
||||
* @param {Array|Uint8Array} aBytes
|
||||
* @param {number|null} [addrLoad]
|
||||
* @param {number|null} [addrExec] (this CAN override any starting address INSIDE the image)
|
||||
* @param {number|null} [addrInit]
|
||||
* @param {boolean} [fStart]
|
||||
* @return {boolean} (true if loaded, false if not)
|
||||
*/
|
||||
loadImage(aBytes, addrLoad, addrExec, addrInit, fStart)
|
||||
{
|
||||
var fStop = false;
|
||||
var fLoaded = false;
|
||||
/*
|
||||
* Data on tapes in the "Absolute Format" is organized into blocks; each block begins with
|
||||
* a 6-byte header:
|
||||
*
|
||||
* 2-byte signature (0x0001)
|
||||
* 2-byte block length (N + 6, because it includes the 6-byte header)
|
||||
* 2-byte load address
|
||||
*
|
||||
* followed by N data bytes. If N is zero, then the 2-byte load address is the exec address,
|
||||
* unless the address is odd (usually 1). DEC's Absolute Loader jumps to the exec address
|
||||
* in former case, halts in the latter.
|
||||
*
|
||||
* All values are stored "little endian" (low byte followed by high byte), just like the
|
||||
* PDP-11's memory architecture.
|
||||
*
|
||||
* After the data bytes, there is a single checksum byte. The 8-bit sum of all the bytes in
|
||||
* the block (including the header bytes and checksum byte) should be zero.
|
||||
*
|
||||
* ANOMALIES: Tape files don't always begin with a signature word, so I allow any number of
|
||||
* leading zeros before the first signature. Tape files don't always end cleanly either, so as
|
||||
* soon as I see an invalid signature, I break out of the loop without signalling an error, as
|
||||
* long as at least ONE block was successfully processed. In fact, it's possible that as
|
||||
* soon as a block with ZERO data bytes is encountered, processing is supposed to stop, but
|
||||
* I haven't examined enough tapes (or the Absolute Loader code) to know for sure.
|
||||
*/
|
||||
if (addrLoad == null) {
|
||||
var off = 0, fError = false;
|
||||
while (off < aBytes.length - 1) {
|
||||
var w = (aBytes[off] & 0xff) | ((aBytes[off+1] & 0xff) << 8);
|
||||
if (!w) { // ignore pairs of leading zeros
|
||||
off += 2;
|
||||
continue;
|
||||
}
|
||||
if (!(w & 0xff)) { // as well as single bytes of zero
|
||||
off++;
|
||||
continue;
|
||||
}
|
||||
var offBlock = off;
|
||||
if (w != 0x0001) {
|
||||
this.printMessage("invalid signature (" + Str.toHexWord(w) + ") at offset " + Str.toHexWord(offBlock), MessagesPDP11.PAPER);
|
||||
break;
|
||||
}
|
||||
if (off + 6 >= aBytes.length) {
|
||||
this.printMessage("invalid block at offset " + Str.toHexWord(offBlock), MessagesPDP11.PAPER);
|
||||
break;
|
||||
}
|
||||
off += 2;
|
||||
var checksum = w;
|
||||
var len = (aBytes[off++] & 0xff) | ((aBytes[off++] & 0xff) << 8);
|
||||
var addr = (aBytes[off++] & 0xff) | ((aBytes[off++] & 0xff) << 8);
|
||||
checksum += (len & 0xff) + (len >> 8) + (addr & 0xff) + (addr >> 8);
|
||||
var offData = off, cbData = len -= 6;
|
||||
while (len > 0 && off < aBytes.length) {
|
||||
checksum += aBytes[off++] & 0xff;
|
||||
len--;
|
||||
}
|
||||
if (len != 0 || off >= aBytes.length) {
|
||||
this.printMessage("insufficient data for block at offset " + Str.toHexWord(offBlock), MessagesPDP11.PAPER);
|
||||
break;
|
||||
}
|
||||
checksum += aBytes[off++] & 0xff;
|
||||
if (checksum & 0xff) {
|
||||
this.printMessage("invalid checksum (" + Str.toHexByte(checksum) + ") for block at offset " + Str.toHexWord(offBlock), MessagesPDP11.PAPER);
|
||||
break;
|
||||
}
|
||||
if (!cbData) {
|
||||
if (addr & 0x1) {
|
||||
fStop = true;
|
||||
} else {
|
||||
if (addrExec == null) addrExec = addr;
|
||||
}
|
||||
if (addrExec != null) this.printMessage("starting address: " + Str.toHexWord(addrExec), MessagesPDP11.PAPER);
|
||||
} else {
|
||||
this.printMessage("loading " + Str.toHexWord(cbData) + " bytes at " + Str.toHexWord(addr) + "-" + Str.toHexWord(addr + cbData), MessagesPDP11.PAPER);
|
||||
while (cbData--) {
|
||||
this.bus.setByteDirect(addr++, aBytes[offData++] & 0xff);
|
||||
}
|
||||
}
|
||||
fLoaded = true;
|
||||
}
|
||||
}
|
||||
if (!fLoaded) {
|
||||
if (addrLoad == null) addrLoad = addrInit;
|
||||
if (addrLoad != null) {
|
||||
for (var i = 0; i < aBytes.length; i++) {
|
||||
this.bus.setByteDirect(addrLoad + i, aBytes[i]);
|
||||
}
|
||||
fLoaded = true;
|
||||
}
|
||||
}
|
||||
if (fLoaded) {
|
||||
/*
|
||||
* Set the start address to whatever the caller provided, or failing that, whatever start
|
||||
* address was specified inside the image.
|
||||
*
|
||||
* For example, the diagnostic "MAINDEC-11-D0AA-PB" doesn't include a start address inside the
|
||||
* image, but we know that the directions for that diagnostic say to "Start and Restart at 200",
|
||||
* so we have manually inserted an "exec":128 in the JSON containing the image.
|
||||
*/
|
||||
if (addrExec == null || fStop) {
|
||||
this.cpu.stopCPU();
|
||||
fStart = false;
|
||||
}
|
||||
if (addrExec != null) {
|
||||
this.cpu.setReset(addrExec, fStart);
|
||||
}
|
||||
}
|
||||
return fLoaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* RAMPDP11.init()
|
||||
*
|
||||
* This function operates on every HTML element of class "ram", extracting the
|
||||
* JSON-encoded parameters for the RAMPDP11 constructor from the element's "data-value"
|
||||
* attribute, invoking the constructor to create a RAMPDP11 component, and then binding
|
||||
* any associated HTML controls to the new component.
|
||||
*/
|
||||
static init()
|
||||
{
|
||||
var aeRAM = Component.getElementsByClass(document, PDP11.APPCLASS, "ram");
|
||||
for (var iRAM = 0; iRAM < aeRAM.length; iRAM++) {
|
||||
var eRAM = aeRAM[iRAM];
|
||||
var parmsRAM = Component.getComponentParms(eRAM);
|
||||
var ram = new RAMPDP11(parmsRAM);
|
||||
Component.bindComponentControls(ram, eRAM, PDP11.APPCLASS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Initialize all the RAMPDP11 modules on the page.
|
||||
*/
|
||||
web.onInit(RAMPDP11.init);
|
||||
Web.onInit(RAMPDP11.init);
|
||||
|
||||
if (NODE) module.exports = RAMPDP11;
|
||||
export default RAMPDP11;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -32,85 +32,343 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
if (NODE) {
|
||||
var str = require("../../shared/lib/strlib");
|
||||
var web = require("../../shared/lib/weblib");
|
||||
var DumpAPI = require("../../shared/lib/dumpapi");
|
||||
var Component = require("../../shared/lib/component");
|
||||
var PDP11 = require("./defines");
|
||||
var BusPDP11 = require("./bus");
|
||||
var MemoryPDP11 = require("./memory");
|
||||
var MessagesPDP11 = require("./messages");
|
||||
}
|
||||
import Str from "../../shared/lib/strlib";
|
||||
import Web from "../../shared/lib/weblib";
|
||||
import DumpAPI from "../../shared/lib/dumpapi";
|
||||
import Component from "../../shared/lib/component";
|
||||
import PDP11 from "./defines";
|
||||
import BusPDP11 from "./bus";
|
||||
import MemoryPDP11 from "./memory";
|
||||
import MessagesPDP11 from "./messages";
|
||||
|
||||
/**
|
||||
* ROMPDP11(parmsROM)
|
||||
*
|
||||
* The ROMPDP11 component expects the following (parmsROM) properties:
|
||||
*
|
||||
* addr: physical address of ROM
|
||||
* size: amount of ROM, in bytes
|
||||
* alias: physical alias address (null if none)
|
||||
* file: name of ROM data file
|
||||
*
|
||||
* NOTE: The ROM data will not be copied into place until the Bus is ready (see initBus()) AND
|
||||
* the ROM data file has finished loading (see finishLoad()).
|
||||
*
|
||||
* Also, while the size parameter may seem redundant, I consider it useful to confirm that the ROM
|
||||
* you received is the ROM you expected.
|
||||
*
|
||||
* @constructor
|
||||
* @extends Component
|
||||
* @param {Object} parmsROM
|
||||
*/
|
||||
function ROMPDP11(parmsROM)
|
||||
{
|
||||
Component.call(this, "ROM", parmsROM, ROMPDP11, MessagesPDP11.ROM);
|
||||
|
||||
this.abInit = null;
|
||||
this.aSymbols = null;
|
||||
|
||||
this.addrROM = parmsROM['addr'];
|
||||
this.sizeROM = parmsROM['size'];
|
||||
this.fRetainROM = false;
|
||||
|
||||
/*
|
||||
* The new 'alias' property can now be EITHER a single physical address (like 'addr') OR an array of
|
||||
* physical addresses; eg:
|
||||
class ROMPDP11 extends Component {
|
||||
/**
|
||||
* ROMPDP11(parmsROM)
|
||||
*
|
||||
* [0xf0000,0xffff0000,0xffff8000]
|
||||
* The ROMPDP11 component expects the following (parmsROM) properties:
|
||||
*
|
||||
* We could have overloaded 'addr' to accomplish the same thing, but I think it's better to have any
|
||||
* aliased locations listed under a separate property.
|
||||
* addr: physical address of ROM
|
||||
* size: amount of ROM, in bytes
|
||||
* alias: physical alias address (null if none)
|
||||
* file: name of ROM data file
|
||||
*
|
||||
* Most ROMs are not aliased, in which case the 'alias' property should have the default value of null.
|
||||
* NOTE: The ROM data will not be copied into place until the Bus is ready (see initBus()) AND
|
||||
* the ROM data file has finished loading (see finishLoad()).
|
||||
*
|
||||
* Also, while the size parameter may seem redundant, I consider it useful to confirm that the ROM
|
||||
* you received is the ROM you expected.
|
||||
*
|
||||
* @param {Object} parmsROM
|
||||
*/
|
||||
this.addrAlias = parmsROM['alias'];
|
||||
constructor(parmsROM)
|
||||
{
|
||||
super("ROM", parmsROM, ROMPDP11, MessagesPDP11.ROM);
|
||||
|
||||
this.sFilePath = parmsROM['file'];
|
||||
this.sFileName = str.getBaseName(this.sFilePath);
|
||||
this.abInit = null;
|
||||
this.aSymbols = null;
|
||||
|
||||
this.addrROM = parmsROM['addr'];
|
||||
this.sizeROM = parmsROM['size'];
|
||||
this.fRetainROM = false;
|
||||
|
||||
if (this.sFilePath) {
|
||||
var sFileURL = this.sFilePath;
|
||||
if (DEBUG) this.log('load("' + sFileURL + '")');
|
||||
/*
|
||||
* If the selected ROM file has a ".json" extension, then we assume it's pre-converted
|
||||
* JSON-encoded ROM data, so we load it as-is; ditto for ROM files with a ".hex" extension.
|
||||
* Otherwise, we ask our server-side ROM converter to return the file in a JSON-compatible format.
|
||||
* The new 'alias' property can now be EITHER a single physical address (like 'addr') OR an array of
|
||||
* physical addresses; eg:
|
||||
*
|
||||
* [0xf0000,0xffff0000,0xffff8000]
|
||||
*
|
||||
* We could have overloaded 'addr' to accomplish the same thing, but I think it's better to have any
|
||||
* aliased locations listed under a separate property.
|
||||
*
|
||||
* Most ROMs are not aliased, in which case the 'alias' property should have the default value of null.
|
||||
*/
|
||||
var sFileExt = str.getExtension(this.sFileName);
|
||||
if (sFileExt != DumpAPI.FORMAT.JSON && sFileExt != DumpAPI.FORMAT.HEX) {
|
||||
sFileURL = web.getHost() + DumpAPI.ENDPOINT + '?' + DumpAPI.QUERY.FILE + '=' + this.sFilePath + '&' + DumpAPI.QUERY.FORMAT + '=' + DumpAPI.FORMAT.BYTES + '&' + DumpAPI.QUERY.DECIMAL + '=true';
|
||||
this.addrAlias = parmsROM['alias'];
|
||||
|
||||
this.sFilePath = parmsROM['file'];
|
||||
this.sFileName = Str.getBaseName(this.sFilePath);
|
||||
|
||||
if (this.sFilePath) {
|
||||
var sFileURL = this.sFilePath;
|
||||
if (DEBUG) this.log('load("' + sFileURL + '")');
|
||||
/*
|
||||
* If the selected ROM file has a ".json" extension, then we assume it's pre-converted
|
||||
* JSON-encoded ROM data, so we load it as-is; ditto for ROM files with a ".hex" extension.
|
||||
* Otherwise, we ask our server-side ROM converter to return the file in a JSON-compatible format.
|
||||
*/
|
||||
var sFileExt = Str.getExtension(this.sFileName);
|
||||
if (sFileExt != DumpAPI.FORMAT.JSON && sFileExt != DumpAPI.FORMAT.HEX) {
|
||||
sFileURL = Web.getHost() + DumpAPI.ENDPOINT + '?' + DumpAPI.QUERY.FILE + '=' + this.sFilePath + '&' + DumpAPI.QUERY.FORMAT + '=' + DumpAPI.FORMAT.BYTES + '&' + DumpAPI.QUERY.DECIMAL + '=true';
|
||||
}
|
||||
var rom = this;
|
||||
Web.getResource(sFileURL, null, true, function doneLoad(sURL, sResponse, nErrorCode) {
|
||||
rom.finishLoad(sURL, sResponse, nErrorCode);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* initBus(cmp, bus, cpu, dbg)
|
||||
*
|
||||
* @this {ROMPDP11}
|
||||
* @param {ComputerPDP11} cmp
|
||||
* @param {BusPDP11} bus
|
||||
* @param {CPUStatePDP11} cpu
|
||||
* @param {DebuggerPDP11} dbg
|
||||
*/
|
||||
initBus(cmp, bus, cpu, dbg)
|
||||
{
|
||||
this.bus = bus;
|
||||
this.cpu = cpu;
|
||||
this.dbg = dbg;
|
||||
this.initROM();
|
||||
}
|
||||
|
||||
/**
|
||||
* powerUp(data, fRepower)
|
||||
*
|
||||
* @this {ROMPDP11}
|
||||
* @param {Object|null} data
|
||||
* @param {boolean} [fRepower]
|
||||
* @return {boolean} true if successful, false if failure
|
||||
*/
|
||||
powerUp(data, fRepower)
|
||||
{
|
||||
if (this.aSymbols) {
|
||||
if (this.dbg) {
|
||||
this.dbg.addSymbols(this.id, this.addrROM, this.sizeROM, this.aSymbols);
|
||||
}
|
||||
/*
|
||||
* Our only role in the handling of symbols is to hand them off to the Debugger at our
|
||||
* first opportunity. Now that we've done that, our copy of the symbols, if any, are toast.
|
||||
*/
|
||||
delete this.aSymbols;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* powerDown(fSave, fShutdown)
|
||||
*
|
||||
* Since we have nothing to do on powerDown(), and no state to return, we could simply omit
|
||||
* this function. But it doesn't hurt anything, and maybe we'll use our state to save something
|
||||
* useful down the road, like user-defined symbols (ie, symbols that the Debugger may have
|
||||
* created, above and beyond those symbols we automatically loaded, if any, along with the ROM).
|
||||
*
|
||||
* @this {ROMPDP11}
|
||||
* @param {boolean} [fSave]
|
||||
* @param {boolean} [fShutdown]
|
||||
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
|
||||
*/
|
||||
powerDown(fSave, fShutdown)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* finishLoad(sURL, sData, nErrorCode)
|
||||
*
|
||||
* @this {ROMPDP11}
|
||||
* @param {string} sURL
|
||||
* @param {string} sData
|
||||
* @param {number} nErrorCode (response from server if anything other than 200)
|
||||
*/
|
||||
finishLoad(sURL, sData, nErrorCode)
|
||||
{
|
||||
if (nErrorCode) {
|
||||
this.notice("Unable to load ROM resource (error " + nErrorCode + ": " + sURL + ")");
|
||||
this.sFilePath = null;
|
||||
}
|
||||
else {
|
||||
Component.addMachineResource(this.idMachine, sURL, sData);
|
||||
var resource = Web.parseMemoryResource(sURL, sData);
|
||||
if (resource) {
|
||||
this.abInit = resource.aBytes;
|
||||
this.aSymbols = resource.aSymbols;
|
||||
} else {
|
||||
this.sFilePath = null;
|
||||
}
|
||||
}
|
||||
this.initROM();
|
||||
}
|
||||
|
||||
/**
|
||||
* initROM()
|
||||
*
|
||||
* This function is called by both initBus() and finishLoad(), but it cannot copy the initial data into place
|
||||
* until after initBus() has received the Bus component AND finishLoad() has received the data. When both those
|
||||
* criteria are satisfied, the component becomes "ready".
|
||||
*
|
||||
* @this {ROMPDP11}
|
||||
*/
|
||||
initROM()
|
||||
{
|
||||
if (!this.isReady()) {
|
||||
if (this.sFilePath) {
|
||||
/*
|
||||
* Too early...
|
||||
*/
|
||||
if (!this.abInit || !this.bus) return;
|
||||
|
||||
/*
|
||||
* If no explicit size was specified, then use whatever the actual size is.
|
||||
*/
|
||||
if (!this.sizeROM) {
|
||||
this.sizeROM = this.abInit.length;
|
||||
}
|
||||
if (this.abInit.length != this.sizeROM) {
|
||||
/*
|
||||
* Note that setError() sets the component's fError flag, which in turn prevents setReady() from
|
||||
* marking the component ready. TODO: Revisit this decision. On the one hand, it sounds like a
|
||||
* good idea to stop the machine in its tracks whenever a setError() occurs, but there may also be
|
||||
* times when we'd like to forge ahead anyway.
|
||||
*/
|
||||
this.setError("ROM size (" + Str.toHexLong(this.abInit.length) + ") does not match specified size (" + Str.toHexLong(this.sizeROM) + ")");
|
||||
}
|
||||
else if (this.addROM(this.addrROM)) {
|
||||
|
||||
var aliases = [];
|
||||
if (typeof this.addrAlias == "number") {
|
||||
aliases.push(this.addrAlias);
|
||||
} else if (this.addrAlias != null && this.addrAlias.length) {
|
||||
aliases = this.addrAlias;
|
||||
}
|
||||
for (var i = 0; i < aliases.length; i++) {
|
||||
this.cloneROM(aliases[i]);
|
||||
}
|
||||
/*
|
||||
* We used to hang onto the initial ROM data so that we could restore any bytes the CPU overwrote,
|
||||
* using memory write-notification handlers, but with the introduction of read-only memory blocks, that's
|
||||
* no longer necessary.
|
||||
*
|
||||
* TODO: Consider an option to retain the ROM data, and give the user some way of restoring ROMs.
|
||||
* That may be useful for "resumable" machines that save/restore all dirty block of memory, regardless
|
||||
* whether they're ROM or RAM. However, the only way to modify a machine's ROM is with the Debugger,
|
||||
* and Debugger users should know better.
|
||||
*/
|
||||
if (!this.fRetainROM) {
|
||||
delete this.abInit;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setReady();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* addROM(addr)
|
||||
*
|
||||
* @this {ROMPDP11}
|
||||
* @param {number} addr
|
||||
* @return {boolean}
|
||||
*/
|
||||
addROM(addr)
|
||||
{
|
||||
this.status(this.sizeROM + "-byte ROM at " + Str.toOct(addr));
|
||||
|
||||
if (addr >= BusPDP11.IOPAGE_16BIT && addr < BusPDP11.IOPAGE_16BIT + BusPDP11.IOPAGE_LENGTH) {
|
||||
/*
|
||||
* This code has been added as a work-around to effectively allow us to install small ROMs into portions
|
||||
* of the IOPAGE address space, by installing I/O handlers for the entire range that return the corresponding
|
||||
* bytes of the current ROM image on reads, and ignore any writes (which I'm only assuming is how a typical
|
||||
* ROM "device" deals with writes; if we remove the write handler, then writes will fault).
|
||||
*
|
||||
* TODO: It would be more efficient if we parsed ROM data as words rather than bytes, and then installed
|
||||
* only word handlers instead of only byte handlers. It was done this way purely for historical reasons (ie,
|
||||
* because that's how other PCjs machines parse their ROMs). For now, all this means is that executing code
|
||||
* out of ROM will be slower than out of RAM -- although that's often true in the real world as well.
|
||||
*/
|
||||
var IOTable = {
|
||||
[addr]: [ROMPDP11.prototype.readROMByte, ROMPDP11.prototype.writeROMByte, null, null, null, this.sizeROM >> 1]
|
||||
};
|
||||
if (this.bus.addIOTable(this, IOTable)) {
|
||||
this.fRetainROM = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (this.bus.addMemory(addr, this.sizeROM, MemoryPDP11.TYPE.ROM)) {
|
||||
if (DEBUG) this.log("addROM(): copying ROM to " + Str.toHexLong(addr) + " (" + Str.toHexLong(this.abInit.length) + " bytes)");
|
||||
var i;
|
||||
for (i = 0; i < this.abInit.length; i++) {
|
||||
this.bus.setByteDirect(addr + i, this.abInit[i]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* We don't need to report an error here, because addMemory() already takes care of that.
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* cloneROM(addr)
|
||||
*
|
||||
* For ROMs with one or more alias addresses, we used to call addROM() for each address. However,
|
||||
* that obviously wasted memory, since each alias was an independent copy, and if you used the
|
||||
* Debugger to edit the ROM in one location, the changes would not appear in the other location(s).
|
||||
*
|
||||
* Now that the Bus component provides low-level getMemoryBlocks() and setMemoryBlocks() methods
|
||||
* to manually get and set the blocks of any memory range, it is now possible to create true aliases.
|
||||
*
|
||||
* @this {ROMPDP11}
|
||||
* @param {number} addr
|
||||
*/
|
||||
cloneROM(addr)
|
||||
{
|
||||
var aBlocks = this.bus.getMemoryBlocks(this.addrROM, this.sizeROM);
|
||||
this.bus.setMemoryBlocks(addr, this.sizeROM, aBlocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* readROMByte(addr)
|
||||
*
|
||||
* @this {ROMPDP11}
|
||||
* @param {number} addr
|
||||
* @return {number}
|
||||
*/
|
||||
readROMByte(addr)
|
||||
{
|
||||
var i = (addr - this.addrROM);
|
||||
return this.abInit[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* writeROMByte(data, addr)
|
||||
*
|
||||
* This handler exists simply to ignore any writes, so that they don't cause faults.
|
||||
*
|
||||
* TODO: Another possible use for this would be to allow the Debugger to alter ROM contents,
|
||||
* if the Debugger were to provide an interface indicating whether or not it was responsible
|
||||
* for this write.
|
||||
*
|
||||
* @this {ROMPDP11}
|
||||
* @param {number} data
|
||||
* @param {number} addr
|
||||
*/
|
||||
writeROMByte(data, addr)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* ROMPDP11.init()
|
||||
*
|
||||
* This function operates on every HTML element of class "rom", extracting the
|
||||
* JSON-encoded parameters for the ROMPDP11 constructor from the element's "data-value"
|
||||
* attribute, invoking the constructor to create a ROMPDP11 component, and then binding
|
||||
* any associated HTML controls to the new component.
|
||||
*/
|
||||
static init()
|
||||
{
|
||||
var aeROM = Component.getElementsByClass(document, PDP11.APPCLASS, "rom");
|
||||
for (var iROM = 0; iROM < aeROM.length; iROM++) {
|
||||
var eROM = aeROM[iROM];
|
||||
var parmsROM = Component.getComponentParms(eROM);
|
||||
var rom = new ROMPDP11(parmsROM);
|
||||
Component.bindComponentControls(rom, eROM, PDP11.APPCLASS);
|
||||
}
|
||||
var rom = this;
|
||||
web.getResource(sFileURL, null, true, function doneLoad(sURL, sResponse, nErrorCode) {
|
||||
rom.finishLoad(sURL, sResponse, nErrorCode);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Component.subclass(ROMPDP11);
|
||||
|
||||
/*
|
||||
* NOTE: There's currently no need for this component to have a reset() function, since
|
||||
* once the ROM data is loaded, it can't be changed, so there's nothing to reinitialize.
|
||||
|
|
@ -124,271 +382,9 @@ Component.subclass(ROMPDP11);
|
|||
* via bus.addMemory().
|
||||
*/
|
||||
|
||||
/**
|
||||
* initBus(cmp, bus, cpu, dbg)
|
||||
*
|
||||
* @this {ROMPDP11}
|
||||
* @param {ComputerPDP11} cmp
|
||||
* @param {BusPDP11} bus
|
||||
* @param {CPUStatePDP11} cpu
|
||||
* @param {DebuggerPDP11} dbg
|
||||
*/
|
||||
ROMPDP11.prototype.initBus = function(cmp, bus, cpu, dbg)
|
||||
{
|
||||
this.bus = bus;
|
||||
this.cpu = cpu;
|
||||
this.dbg = dbg;
|
||||
this.initROM();
|
||||
};
|
||||
|
||||
/**
|
||||
* powerUp(data, fRepower)
|
||||
*
|
||||
* @this {ROMPDP11}
|
||||
* @param {Object|null} data
|
||||
* @param {boolean} [fRepower]
|
||||
* @return {boolean} true if successful, false if failure
|
||||
*/
|
||||
ROMPDP11.prototype.powerUp = function(data, fRepower)
|
||||
{
|
||||
if (this.aSymbols) {
|
||||
if (this.dbg) {
|
||||
this.dbg.addSymbols(this.id, this.addrROM, this.sizeROM, this.aSymbols);
|
||||
}
|
||||
/*
|
||||
* Our only role in the handling of symbols is to hand them off to the Debugger at our
|
||||
* first opportunity. Now that we've done that, our copy of the symbols, if any, are toast.
|
||||
*/
|
||||
delete this.aSymbols;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* powerDown(fSave, fShutdown)
|
||||
*
|
||||
* Since we have nothing to do on powerDown(), and no state to return, we could simply omit
|
||||
* this function. But it doesn't hurt anything, and maybe we'll use our state to save something
|
||||
* useful down the road, like user-defined symbols (ie, symbols that the Debugger may have
|
||||
* created, above and beyond those symbols we automatically loaded, if any, along with the ROM).
|
||||
*
|
||||
* @this {ROMPDP11}
|
||||
* @param {boolean} [fSave]
|
||||
* @param {boolean} [fShutdown]
|
||||
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
|
||||
*/
|
||||
ROMPDP11.prototype.powerDown = function(fSave, fShutdown)
|
||||
{
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* finishLoad(sURL, sData, nErrorCode)
|
||||
*
|
||||
* @this {ROMPDP11}
|
||||
* @param {string} sURL
|
||||
* @param {string} sData
|
||||
* @param {number} nErrorCode (response from server if anything other than 200)
|
||||
*/
|
||||
ROMPDP11.prototype.finishLoad = function(sURL, sData, nErrorCode)
|
||||
{
|
||||
if (nErrorCode) {
|
||||
this.notice("Unable to load ROM resource (error " + nErrorCode + ": " + sURL + ")");
|
||||
this.sFilePath = null;
|
||||
}
|
||||
else {
|
||||
Component.addMachineResource(this.idMachine, sURL, sData);
|
||||
var resource = web.parseMemoryResource(sURL, sData);
|
||||
if (resource) {
|
||||
this.abInit = resource.aBytes;
|
||||
this.aSymbols = resource.aSymbols;
|
||||
} else {
|
||||
this.sFilePath = null;
|
||||
}
|
||||
}
|
||||
this.initROM();
|
||||
};
|
||||
|
||||
/**
|
||||
* initROM()
|
||||
*
|
||||
* This function is called by both initBus() and finishLoad(), but it cannot copy the initial data into place
|
||||
* until after initBus() has received the Bus component AND finishLoad() has received the data. When both those
|
||||
* criteria are satisfied, the component becomes "ready".
|
||||
*
|
||||
* @this {ROMPDP11}
|
||||
*/
|
||||
ROMPDP11.prototype.initROM = function()
|
||||
{
|
||||
if (!this.isReady()) {
|
||||
if (this.sFilePath) {
|
||||
/*
|
||||
* Too early...
|
||||
*/
|
||||
if (!this.abInit || !this.bus) return;
|
||||
|
||||
/*
|
||||
* If no explicit size was specified, then use whatever the actual size is.
|
||||
*/
|
||||
if (!this.sizeROM) {
|
||||
this.sizeROM = this.abInit.length;
|
||||
}
|
||||
if (this.abInit.length != this.sizeROM) {
|
||||
/*
|
||||
* Note that setError() sets the component's fError flag, which in turn prevents setReady() from
|
||||
* marking the component ready. TODO: Revisit this decision. On the one hand, it sounds like a
|
||||
* good idea to stop the machine in its tracks whenever a setError() occurs, but there may also be
|
||||
* times when we'd like to forge ahead anyway.
|
||||
*/
|
||||
this.setError("ROM size (" + str.toHexLong(this.abInit.length) + ") does not match specified size (" + str.toHexLong(this.sizeROM) + ")");
|
||||
}
|
||||
else if (this.addROM(this.addrROM)) {
|
||||
|
||||
var aliases = [];
|
||||
if (typeof this.addrAlias == "number") {
|
||||
aliases.push(this.addrAlias);
|
||||
} else if (this.addrAlias != null && this.addrAlias.length) {
|
||||
aliases = this.addrAlias;
|
||||
}
|
||||
for (var i = 0; i < aliases.length; i++) {
|
||||
this.cloneROM(aliases[i]);
|
||||
}
|
||||
/*
|
||||
* We used to hang onto the initial ROM data so that we could restore any bytes the CPU overwrote,
|
||||
* using memory write-notification handlers, but with the introduction of read-only memory blocks, that's
|
||||
* no longer necessary.
|
||||
*
|
||||
* TODO: Consider an option to retain the ROM data, and give the user some way of restoring ROMs.
|
||||
* That may be useful for "resumable" machines that save/restore all dirty block of memory, regardless
|
||||
* whether they're ROM or RAM. However, the only way to modify a machine's ROM is with the Debugger,
|
||||
* and Debugger users should know better.
|
||||
*/
|
||||
if (!this.fRetainROM) {
|
||||
delete this.abInit;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setReady();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* addROM(addr)
|
||||
*
|
||||
* @this {ROMPDP11}
|
||||
* @param {number} addr
|
||||
* @return {boolean}
|
||||
*/
|
||||
ROMPDP11.prototype.addROM = function(addr)
|
||||
{
|
||||
this.status(this.sizeROM + "-byte ROM at " + str.toOct(addr));
|
||||
|
||||
if (addr >= BusPDP11.IOPAGE_16BIT && addr < BusPDP11.IOPAGE_16BIT + BusPDP11.IOPAGE_LENGTH) {
|
||||
/*
|
||||
* This code has been added as a work-around to effectively allow us to install small ROMs into portions
|
||||
* of the IOPAGE address space, by installing I/O handlers for the entire range that return the corresponding
|
||||
* bytes of the current ROM image on reads, and ignore any writes (which I'm only assuming is how a typical
|
||||
* ROM "device" deals with writes; if we remove the write handler, then writes will fault).
|
||||
*
|
||||
* TODO: It would be more efficient if we parsed ROM data as words rather than bytes, and then installed
|
||||
* only word handlers instead of only byte handlers. It was done this way purely for historical reasons (ie,
|
||||
* because that's how other PCjs machines parse their ROMs). For now, all this means is that executing code
|
||||
* out of ROM will be slower than out of RAM -- although that's often true in the real world as well.
|
||||
*/
|
||||
var IOTable = {
|
||||
[addr]: [ROMPDP11.prototype.readROMByte, ROMPDP11.prototype.writeROMByte, null, null, null, this.sizeROM >> 1]
|
||||
};
|
||||
if (this.bus.addIOTable(this, IOTable)) {
|
||||
this.fRetainROM = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (this.bus.addMemory(addr, this.sizeROM, MemoryPDP11.TYPE.ROM)) {
|
||||
if (DEBUG) this.log("addROM(): copying ROM to " + str.toHexLong(addr) + " (" + str.toHexLong(this.abInit.length) + " bytes)");
|
||||
var i;
|
||||
for (i = 0; i < this.abInit.length; i++) {
|
||||
this.bus.setByteDirect(addr + i, this.abInit[i]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* We don't need to report an error here, because addMemory() already takes care of that.
|
||||
*/
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* cloneROM(addr)
|
||||
*
|
||||
* For ROMs with one or more alias addresses, we used to call addROM() for each address. However,
|
||||
* that obviously wasted memory, since each alias was an independent copy, and if you used the
|
||||
* Debugger to edit the ROM in one location, the changes would not appear in the other location(s).
|
||||
*
|
||||
* Now that the Bus component provides low-level getMemoryBlocks() and setMemoryBlocks() methods
|
||||
* to manually get and set the blocks of any memory range, it is now possible to create true aliases.
|
||||
*
|
||||
* @this {ROMPDP11}
|
||||
* @param {number} addr
|
||||
*/
|
||||
ROMPDP11.prototype.cloneROM = function(addr)
|
||||
{
|
||||
var aBlocks = this.bus.getMemoryBlocks(this.addrROM, this.sizeROM);
|
||||
this.bus.setMemoryBlocks(addr, this.sizeROM, aBlocks);
|
||||
};
|
||||
|
||||
/**
|
||||
* readROMByte(addr)
|
||||
*
|
||||
* @this {ROMPDP11}
|
||||
* @param {number} addr
|
||||
* @return {number}
|
||||
*/
|
||||
ROMPDP11.prototype.readROMByte = function(addr)
|
||||
{
|
||||
var i = (addr - this.addrROM);
|
||||
return this.abInit[i];
|
||||
};
|
||||
|
||||
/**
|
||||
* writeROMByte(data, addr)
|
||||
*
|
||||
* This handler exists simply to ignore any writes, so that they don't cause faults.
|
||||
*
|
||||
* TODO: Another possible use for this would be to allow the Debugger to alter ROM contents,
|
||||
* if the Debugger were to provide an interface indicating whether or not it was responsible
|
||||
* for this write.
|
||||
*
|
||||
* @this {ROMPDP11}
|
||||
* @param {number} data
|
||||
* @param {number} addr
|
||||
*/
|
||||
ROMPDP11.prototype.writeROMByte = function(data, addr)
|
||||
{
|
||||
};
|
||||
|
||||
/**
|
||||
* ROMPDP11.init()
|
||||
*
|
||||
* This function operates on every HTML element of class "rom", extracting the
|
||||
* JSON-encoded parameters for the ROMPDP11 constructor from the element's "data-value"
|
||||
* attribute, invoking the constructor to create a ROMPDP11 component, and then binding
|
||||
* any associated HTML controls to the new component.
|
||||
*/
|
||||
ROMPDP11.init = function()
|
||||
{
|
||||
var aeROM = Component.getElementsByClass(document, PDP11.APPCLASS, "rom");
|
||||
for (var iROM = 0; iROM < aeROM.length; iROM++) {
|
||||
var eROM = aeROM[iROM];
|
||||
var parmsROM = Component.getComponentParms(eROM);
|
||||
var rom = new ROMPDP11(parmsROM);
|
||||
Component.bindComponentControls(rom, eROM, PDP11.APPCLASS);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Initialize all the ROMPDP11 modules on the page.
|
||||
*/
|
||||
web.onInit(ROMPDP11.init);
|
||||
Web.onInit(ROMPDP11.init);
|
||||
|
||||
if (NODE) module.exports = ROMPDP11;
|
||||
export default ROMPDP11;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
1056
modules/shared/es6/component.js
Normal file
1056
modules/shared/es6/component.js
Normal file
File diff suppressed because it is too large
Load diff
677
modules/shared/es6/debugger.js
Normal file
677
modules/shared/es6/debugger.js
Normal 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;
|
||||
112
modules/shared/es6/defines.js
Normal file
112
modules/shared/es6/defines.js
Normal 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
|
||||
}
|
||||
};
|
||||
242
modules/shared/es6/diskapi.js
Normal file
242
modules/shared/es6/diskapi.js
Normal 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;
|
||||
85
modules/shared/es6/dumpapi.js
Normal file
85
modules/shared/es6/dumpapi.js
Normal 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
613
modules/shared/es6/embed.js
Normal 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;
|
||||
34
modules/shared/es6/externs.js
Normal file
34
modules/shared/es6/externs.js
Normal 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
251
modules/shared/es6/keys.js
Normal 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;
|
||||
390
modules/shared/es6/netlib.js
Normal file
390
modules/shared/es6/netlib.js
Normal 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;
|
||||
39
modules/shared/es6/nodebug.js
Normal file
39
modules/shared/es6/nodebug.js
Normal 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;
|
||||
39
modules/shared/es6/private.js
Normal file
39
modules/shared/es6/private.js
Normal 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;
|
||||
87
modules/shared/es6/proclib.js
Normal file
87
modules/shared/es6/proclib.js
Normal 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;
|
||||
49
modules/shared/es6/reportapi.js
Normal file
49
modules/shared/es6/reportapi.js
Normal 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
219
modules/shared/es6/save.js
Normal 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, "©");
|
||||
|
||||
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
395
modules/shared/es6/state.js
Normal 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;
|
||||
620
modules/shared/es6/strlib.js
Normal file
620
modules/shared/es6/strlib.js
Normal 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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
|
||||
/*
|
||||
* 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;
|
||||
68
modules/shared/es6/userapi.js
Normal file
68
modules/shared/es6/userapi.js
Normal 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;
|
||||
324
modules/shared/es6/usrlib.js
Normal file
324
modules/shared/es6/usrlib.js
Normal 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;
|
||||
987
modules/shared/es6/weblib.js
Normal file
987
modules/shared/es6/weblib.js
Normal 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;
|
||||
Loading…
Reference in a new issue