diff --git a/_posts/2017-06-15-adventures-in-archiving.md b/_posts/2017-06-15-adventures-in-archiving.md index e4ed7011f..417108fba 100644 --- a/_posts/2017-06-15-adventures-in-archiving.md +++ b/_posts/2017-06-15-adventures-in-archiving.md @@ -12,7 +12,7 @@ machines: path: /disks/pcx86/dos/ibm/1.00/PCDOS100.json B: path: /apps/pcx86/1983/adventmath/ADVENTMATH100.json - autoType: 8-10-81\\rb:\\ra:basica castle\\r + autoType: 8-10-81\rb:\ra:basica castle\r --- In October 1983, IBM released a game called "Adventures in Math", which apparently tried to make mathematics as diff --git a/apps/pcx86/1981/visicalc/README.md b/apps/pcx86/1981/visicalc/README.md index a3eb5e631..c796eba48 100644 --- a/apps/pcx86/1981/visicalc/README.md +++ b/apps/pcx86/1981/visicalc/README.md @@ -12,7 +12,7 @@ machines: path: /disks/pcx86/dos/ibm/1.00/PCDOS100.json B: path: /apps/pcx86/1981/visicalc/VISICALC1981.json - autoType: 8-10-81\\rb:\\rvc\\r + autoType: 8-10-81\rb:\rvc\r --- VisiCalc (1981) diff --git a/apps/pcx86/1982/ratbas/README.md b/apps/pcx86/1982/ratbas/README.md index 0c273470f..3cf388908 100644 --- a/apps/pcx86/1982/ratbas/README.md +++ b/apps/pcx86/1982/ratbas/README.md @@ -4,14 +4,14 @@ title: "RatBas (1982)" permalink: /apps/pcx86/1982/ratbas/ machines: - type: pcx86 - id: ibm5150 + id: ibm5150-ratbas config: /devices/pcx86/machine/5150/cga/256kb/machine.xml autoMount: A: path: /disks/pcx86/dos/ibm/1.10/PCDOS110.json B: path: /apps/pcx86/1982/ratbas/RATBAS213.json - autoType: \\r\\rb:\\rrt\\r\\xff\\xf0test;\\r + autoType: \r\rb:\rrt\r\xff\xf0test;\r --- RatBas (1982) @@ -30,7 +30,7 @@ We have archived it as [RatBas 2.13](#directory-listing-of-ratbas-213). It has the machine below, which is running [PC-DOS 1.10](/disks/pcx86/dos/ibm/1.10/), the most recent version of PC-DOS at the time of this RatBas release. -{% include machine.html id="ibm5150" %} +{% include machine.html id="ibm5150-ratbas" %} Directory Listing of RatBas 2.13 -------------------------------- diff --git a/apps/pcx86/1983/adventmath/README.md b/apps/pcx86/1983/adventmath/README.md index 729fa5958..d38de2ac6 100644 --- a/apps/pcx86/1983/adventmath/README.md +++ b/apps/pcx86/1983/adventmath/README.md @@ -11,7 +11,7 @@ machines: path: /disks/pcx86/dos/ibm/1.00/PCDOS100.json B: path: /apps/pcx86/1983/adventmath/ADVENTMATH100.json - autoType: 8-10-81\\rb:\\ra:basica castle\\r + autoType: 8-10-81\rb:\ra:basica castle\r --- Adventures in Math (1983) @@ -186,7 +186,7 @@ have been tested with PC-DOS 2.00 on a 64Kb machine, but perhaps it wasn't. The machine on this page is configured with the following "autoType" sequence: - autoType: 8-10-81\\rb:\\ra:basica castle\\r + autoType: 8-10-81\rb:\ra:basica castle\r which translates to: diff --git a/devices/pcx86/machine/5150/cga/256kb/machine.xml b/devices/pcx86/machine/5150/cga/256kb/machine.xml index 1bf512b0a..ec9675195 100644 --- a/devices/pcx86/machine/5150/cga/256kb/machine.xml +++ b/devices/pcx86/machine/5150/cga/256kb/machine.xml @@ -2,13 +2,17 @@ IBM PC (Model 5150), CGA, 256K - - + diff --git a/versions/c1pjs/1.35.0/c1p-uncompiled.js b/versions/c1pjs/1.35.0/c1p-uncompiled.js new file mode 100644 index 000000000..64abce0c5 --- /dev/null +++ b/versions/c1pjs/1.35.0/c1p-uncompiled.js @@ -0,0 +1,14262 @@ +"use strict"; + +/** + * @copyright http://pcjs.org/modules/shared/lib/defines.js (C) Jeff Parsons 2012-2017 + */ + +/** + * @define {string} + */ +var APPVERSION = "1.x.x"; // this @define is overridden by the Closure Compiler with the version in package.json + +var XMLVERSION = null; // this is set in non-COMPILED builds by embedMachine() if a version number was found in the machine XML + +var COPYRIGHT = "Copyright © 2012-2017 Jeff Parsons "; + +var LICENSE = "License: GPL version 3 or later "; + +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 + } +}; + +/* + * NODE should be true if we're running under NodeJS (eg, command-line), false if not (eg, web browser) + */ +var NODE = false; + + +/** + * @copyright http://pcjs.org/modules/shared/lib/dumpapi.js (C) Jeff Parsons 2012-2017 + */ + +/* + * 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 + LONGS: "longs", // displays data as dwords + 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]; + + + +/** + * @copyright http://pcjs.org/modules/shared/lib/reportapi.js (C) Jeff Parsons 2012-2017 + */ + +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" + } +}; + + + +/** + * @copyright http://pcjs.org/modules/shared/lib/strlib.js (C) Jeff Parsons 2012-2017 + */ + +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(). + * + * More recently, we've added support for "^D", "^O", and "^B" prefixes to accommodate the base overrides + * that the PDP-10's MACRO-10 assembly language supports (decimal, octal, and binary, respectively). + * If this support turns out to adversely affect other debuggers, then it will have to be "conditionalized". + * Similarly, we've added support for "K", "M", and "G" MACRO-10-style suffixes that add 3, 6, or 9 zeros + * to the value to be parsed, respectively. + * + * @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 ch, chPrefix, chSuffix; + var fCommas = (s.indexOf(',') > 0); + if (fCommas) s = s.replace(/,/g, ''); + + ch = chPrefix = s.charAt(0); + if (chPrefix == '#') { + base = 8; + chPrefix = ''; + } + else if (chPrefix == '$') { + base = 16; + chPrefix = ''; + } + if (ch != chPrefix) { + s = s.substr(1); + } + else { + ch = chPrefix = s.substr(0, 2); + if (chPrefix == '0b' && fCommas || chPrefix == '^B') { + base = 2; + chPrefix = ''; + } + else if (chPrefix == '0o' || chPrefix == '^O') { + base = 8; + chPrefix = ''; + } + else if (chPrefix == '^D') { + base = 10; + chPrefix = ''; + } + else if (chPrefix == '0x') { + base = 16; + chPrefix = ''; + } + if (ch != chPrefix) s = s.substr(2); + } + ch = chSuffix = s.slice(-1); + if (chSuffix == 'Y' || chSuffix == 'y') { + base = 2; + chSuffix = ''; + } + else if (chSuffix == '.') { + base = 10; + chSuffix = ''; + } + else if (chSuffix == 'H' || chSuffix == 'h') { + base = 16; + chSuffix = ''; + } + else if (chSuffix == 'K') { + chSuffix = '000'; + } + else if (chSuffix == 'M') { + chSuffix = '000000'; + } + else if (chSuffix == 'G') { + chSuffix = '000000000'; + } + if (ch != chSuffix) s = s.slice(0, -1) + chSuffix; + /* + * This adds support for the MACRO-10 binary shifting (Bn) suffix, which must be stripped from the + * number before parsing, and then applied to the value after parsing. If n is omitted, 35 is assumed, + * which is a net shift of zero. If n < 35, then a left shift of (35 - n) is required; if n > 35, then + * a right shift of -(35 - n) is required. + */ + var v, shift = 0; + if (base <= 10) { + var match = s.match(/(-?[0-9]+)B([0-9]*)/); + if (match) { + s = match[1]; + shift = 35 - ((match[2] || 35) & 0xff); + } + } + if (Str.isValidInt(s, base) && !isNaN(v = parseInt(s, base))) { + /* + * With the need to support larger (eg, 36-bit) integers, truncating to 32 bits is no longer helpful. + * + * value = v|0; + */ + if (shift) { + /* + * Since binary shifting is a logical operation, and since shifting by division only works properly + * with positive numbers, we must convert a negative value to a positive value, by computing the two's + * complement. + */ + if (v < 0) v += Math.pow(2, 36); + if (shift > 0) { + v *= Math.pow(2, shift); + } else { + v = Math.trunc(v / Math.pow(2, -shift)); + } + } + value = v; + } + } + return value; + } + + /** + * toBase(n, radix, cch, sPrefix, nGrouping) + * + * Displays the given number as an unsigned integer using the specified radix and number of digits. + * + * @param {number|null|undefined} n + * @param {number} radix (ie, the base) + * @param {number} cch (the desired number of digits) + * @param {string} [sPrefix] (default is none) + * @param {number} [nGrouping] + * @return {string} + */ + static toBase(n, radix, cch, sPrefix = "", nGrouping = 0) + { + /* + * 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 s = ""; + if (isNaN(n)) { + n = null; + } else if (n != null) { + /* + * Callers that produced an input by dividing by a power of two rather than shifting (in order + * to access more than 32 bits) may produce a fractional result, which ordinarily we would simply + * ignore, but if the integer portion is zero and the sign is negative, we should probably treat + * this value as a sign-extension. + */ + if (n < 0 && n > -1) n = -1; + /* + * Negative values should be two's complemented according to the number of digits; for example, + * 12 octal digits implies an upper limit 8^12. + */ + if (n < 0) { + n += Math.pow(radix, cch); + } + if (n >= Math.pow(radix, cch)) { + cch = Math.ceil(Math.log(n) / Math.log(radix)); + } + } + var g = nGrouping || -1; + while (cch-- > 0) { + if (!g) { + s = ',' + s; + g = nGrouping; + } + if (n == null) { + s = '?' + s; + } else { + var d = n % radix; + d += (d >= 0 && d <= 9? 0x30 : 0x41 - 10); + s = String.fromCharCode(d) + s; + n = Math.trunc(n / radix); + } + g--; + } + return sPrefix + s; + } + + /** + * toBin(n, cch, nGrouping) + * + * Converts an integer to binary, with the specified number of digits (up to a maximum of 36). + * + * @param {number|null|undefined} n (supports integers up to 36 bits now) + * @param {number} [cch] is the desired number of binary digits (0 or undefined for default of either 8, 18, or 36) + * @param {number} [nGrouping] + * @return {string} the binary representation of n + */ + static toBin(n, cch, nGrouping) + { + if (!cch) { + // cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.LN2) || 1; + var v = Math.abs(n); + if (v <= 0b11111111) { + cch = 8; + } else if (v <= 0b111111111111111111) { + cch = 18; + } else { + cch = 36; + } + } else if (cch > 36) cch = 36; + return Str.toBase(n, 2, cch, "", nGrouping); + } + + /** + * 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 (interpreted as 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 12) + * + * 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 (supports integers up to 36 bits now) + * @param {number} [cch] is the desired number of octal digits (0 or undefined for default of either 6, 8, or 12) + * @param {boolean} [fPrefix] + * @return {string} the octal representation of n + */ + static toOct(n, cch, fPrefix) + { + if (!cch) { + // cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.log(8)) || 1; + var v = Math.abs(n); + if (v <= 0o777777) { + cch = 6; + } else if (v <= 0o77777777) { + cch = 8; + } else { + cch = 12; + } + } else if (cch > 12) cch = 12; + return Str.toBase(n, 8, cch, fPrefix? "0o" : ""); + } + + /** + * toDec(n, cch) + * + * Converts an integer to decimal, with the specified number of digits (default of 5; max of 11) + * + * 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 (supports integers up to 36 bits now) + * @param {number} [cch] is the desired number of decimal digits (0 or undefined for default of either 5 or 11) + * @return {string} the decimal representation of n + */ + static toDec(n, cch) + { + if (!cch) { + // cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.LN10) || 1; + var v = Math.abs(n); + if (v <= 99999) { + cch = 5; + } else { + cch = 11; + } + } else if (cch > 11) cch = 11; + return Str.toBase(n, 10, cch); + } + + /** + * toHex(n, cch, fPrefix) + * + * Converts an integer to hex, with the specified number of digits (default of 4 or 8, max of 9). + * + * 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 (supports integers up to 36 bits now) + * @param {number} [cch] is the desired number of hex digits (0 or undefined for default of either 4, 8, or 9) + * @param {boolean} [fPrefix] + * @return {string} the hex representation of n + */ + static toHex(n, cch, fPrefix) + { + if (!cch) { + // cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.log(16)) || 1; + var v = Math.abs(n); + if (v <= 0xffff) { + cch = 4; + } else if (v <= 0xffffffff) { + cch = 8; + } else { + cch = 9; + } + } else if (cch > 9) cch = 9; + return Str.toBase(n, 16, cch, fPrefix? "0x" : ""); + } + + /** + * 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; + if (b != Str.ASCII.CR && b != Str.ASCII.LF) { + s = Str.aASCIICodes[b]; + } + 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 +}; + +Str.TYPES = { + NULL: 0, + BYTE: 1, + WORD: 2, + DWORD: 3, + NUMBER: 4, + STRING: 5, + BOOLEAN: 6, + OBJECT: 7, + ARRAY: 8 +}; + + + +/** + * @copyright http://pcjs.org/modules/shared/lib/usrlib.js (C) Jeff Parsons 2012-2017 + */ + +/** + * @typedef {{ + * mask: number, + * shift: number + * }} + */ +var BitField; + +/** + * @typedef {Object.} + */ +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); + } + } + + /** + * 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; + } + return bfs; + } + + /** + * initBitFields(bfs, ...) + * + * @param {BitFields} bfs + * @param {...number} var_args + * @return {number} a value containing all supplied bit fields + */ + static initBitFields(bfs, var_args) + { + var v = 0, i = 1; + for (var f in bfs) { + if (i >= arguments.length) break; + v = Usr.setBitField(bfs[f], v, arguments[i++]); + } + return v; + } + + /** + * getBitField(bf, v) + * + * @param {BitField} bf + * @param {number} v is a value containing bit fields + * @return {number} the value of the bit field in v defined by bf + */ + static getBitField(bf, v) + { + return (v & bf.mask) >> bf.shift; + } + + /** + * setBitField(bf, v, n) + * + * @param {BitField} bf + * @param {number} v is a value containing bit fields + * @param {number} n is a value to store in v in the bit field defined by bf + * @return {number} updated v + */ + static setBitField(bf, v, n) + { + return (v & ~bf.mask) | ((n << bf.shift) & bf.mask); + } + + /** + * indexOf(a, t, i) + * + * Use this instead of Array.prototype.indexOf() if you can't be sure the browser supports it. + * + * @param {Array} a + * @param {*} t + * @param {number} [i] + * @returns {number} + */ + static indexOf(a, t, i) + { + if (Array.prototype.indexOf) { + return a.indexOf(t, i); + } + i = i || 0; + if (i < 0) i += a.length; + if (i < 0) i = 0; + for (var n = a.length; i < n; i++) { + if (i in a && a[i] === t) return i; + } + return -1; + } +} + +Usr.asDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; +Usr.asMonths = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; +Usr.aMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + +/** + * getTime() + * + * @return {number} the current time, in milliseconds + */ +Usr.getTime = Date.now || function() { return +new Date(); }; + + + +/** + * @copyright http://pcjs.org/modules/shared/lib/weblib.js (C) Jeff Parsons 2012-2017 + */ + + +/* + * 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) + * + * @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(). + * + * If fAsync is true, a done() callback should ALWAYS be supplied; otherwise, you'll have no + * idea when the request is complete or what the response was. done() is passed three parameters: + * + * done(sURL, sResource, nErrorCode) + * + * If nErrorCode is zero, sResource should contain the requested data; otherwise, an error occurred. + * + * 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(). + * + * @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) + * + * This converts a variety of JSON-style data streams into an Object with the following properties: + * + * aBytes + * aSymbols + * addrLoad + * addrExec + * + * If the source data contains a 'bytes' array, it's passed through to 'aBytes'; alternatively, if + * it contains a 'words' array, the values are converted from 16-bit to 8-bit and stored in 'aBytes', + * and if it contains a 'longs' array, the values are converted from 32-bit longs into bytes and + * stored in 'aBytes'. + * + * Alternatively, if the source data contains a 'data' array, we simply pass that through to the output + * object as: + * + * aData + * + * @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.indexOf("0o") < 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; + + } + } + else if (a = data['longs']) { + /* + * 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 if (a = data['data']) { + resource.aData = a; + } + else { + resource.aBytes = data; + } + + if (resource.aBytes) { + if (!resource.aBytes.length) { + Component.error("Empty resource: " + sURL); + resource = null; + } + else if (resource.aBytes.length == 1) { + Component.error(resource.aBytes[0]); + resource = null; + } + } + resource.aSymbols = data['symbols']; + + } 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 : ""); + } + + /** + * 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"); + } + + /** + * getURLParm(sParm) + * + * @param {string} sParm + * @return {string|undefined} + */ + static getURLParm(sParm) + { + if (!Web.parmsURL) { + Web.parmsURL = Web.parseURLParms(); + } + return Web.parmsURL[sParm]; + } + + /** + * parseURLParms(sParms) + * + * @param {string} [sParms] containing the parameter portion of a URL (ie, after the '?') + * @return {Object} containing properties for each parameter found + */ + static parseURLParms(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.} 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.parmsURL = null; // initialized on first call to parseURLParms() + +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']); +}); + + + +/** + * @copyright http://pcjs.org/modules/shared/lib/component.js (C) Jeff Parsons 2012-2017 + */ + +/* + * All PCjs components now use JSDoc types, primarily so that Google's Closure Compiler will compile + * everything with zero warnings when ADVANCED_OPTIMIZATIONS are enabled. For more information about + * the JSDoc types supported by the Closure Compiler: + * + * https://developers.google.com/closure/compiler/docs/js-for-compiler#types + * + * I also attempted to validate this code with JSLint, but it complained too much; eg, it didn't like + * "while (true)", a tried and "true" programming convention for decades, and it wanted me to replace + * all "++" and "--" operators with "+= 1" and "-= 1", use "(s || '')" instead of "(s? s : '')", etc. + * + * I prefer sticking with traditional C-style idioms, in part because they are more portable. That + * does NOT mean I'm trying to write "portable JavaScript," but some of this code was ported from C code + * I'd written long ago, so portability is good, and I'm not going to throw that away if there's no need. + * + * UPDATE: I've since switched from JSLint to JSHint, which seems to have more reasonable defaults. + * And for new code, I have adopted some popular JavaScript idioms, like "(s || '')", although the need + * for those kinds of expressions will be reduced as I also start adopting some ES6 features, like + * default parameters. + */ + + +/** + * Since the Closure Compiler treats ES6 classes as @struct rather than @dict by default, + * it deters us from defining named properties on our components; eg: + * + * this['exports'] = {...} + * + * results in an error: + * + * Cannot do '[]' access on a struct + * + * So, in order to define 'exports', we must override the @struct assumption by annotating + * the class as @unrestricted (or @dict). Note that this must be done both here and in the + * subclass (eg, SerialPort), because otherwise the Compiler won't allow us to *reference* + * the named property either. + * + * TODO: Consider marking ALL our classes unrestricted, because otherwise it forces us to + * define every single property the class uses in its constructor, which results in a fair + * bit of redundant initialization, since many properties aren't (and don't need to be) fully + * initialized until the appropriate init(), reset(), restore(), etc. function is called. + * + * The upside, however, may be that since the structure of the class is completely defined by + * the constructor, JavaScript engines may be able to optimize and run more efficiently. + * + * @unrestricted + */ +class Component { + /** + * Component(type, parms, bitsMessage) + * + * A Component object requires: + * + * type: a user-defined type name (eg, "CPU") + * + * and accepts any or all of the following (parms) properties: + * + * id: component ID (default is "") + * name: component name (default is ""; if blank, toString() will use the type name only) + * comment: component comment string (default is undefined) + * + * Component subclasses will usually have additional (parms) properties. + * + * @param {string} type + * @param {Object} [parms] + * @param {number} [bitsMessage] selects message(s) that the component wants to enable (default is 0) + */ + constructor(type, parms, bitsMessage) + { + this.type = type; + + if (!parms) parms = {'id': "", 'name': ""}; + + this.id = parms['id'] || ""; + this.name = parms['name']; + this.comment = parms['comment']; + this.parms = parms; + + /* + * The following Component properties need to be accessible by other machines and/or command scripts; + * well, OK, or we could have exported some new functions to walk the contents of these properties, as we + * did with findMachineComponent(), but this works just as well. + * + * Also, while the double-assignment looks silly (ie, using both dot and bracket property notation), it + * resolves a complaint from the Closure Compiler, because if we use ONLY bracket notation here, then the + * Compiler wants us to change all the other references to bracket notation as well. + */ + this.exports = this['exports'] = {}; + this.bindings = this['bindings'] = {}; + + var i = this.id.indexOf('.'); + if (i < 0) { + this.idComponent = this.id; + } else { + this.idMachine = this.id.substr(0, i); + this.idComponent = this.id.substr(i + 1); + } + + /* + * Gather all the various component flags (booleans) into a single "flags" object, and encourage + * subclasses to do the same, to reduce the property clutter we have to wade through while debugging. + */ + this.flags = { + ready: false, + busy: false, + busyCancel: false, + powered: false, + error: false + }; + + this.fnReady = null; + this.clearError(); + this.bitsMessage = bitsMessage || 0; + + /** @type {Object|null} controlPrint is the HTML control, if any, that we can print to */ + this.controlPrint = null; + + this.cmp = null; + this.bus = null; + this.cpu = null; + this.dbg = null; + + /* + * TODO: Consider adding another parameter to the Component() constructor that allows components to tell + * us if they support single or multiple instances per machine. For example, there can be multiple SerialPort + * components per machine, but only one CPU component (some machines also support an FPU, but that component + * is considered separate from the CPU). + * + * It's not critical, but it would help catch machine configuration errors; for example, a machine that mistakenly + * includes two CPU components may, aside from wasting memory, end up with odd side-effects, like unresponsive + * CPU controls. + */ + Component.add(this); + } + + /** + * Component.add(component) + * + * @param {Component} component + */ + static add(component) + { + /* + * This just generates a lot of useless noise, handy in the early days, not so much these days.... + * + * if (DEBUG) Component.log("Component.add(" + component.type + "," + component.id + ")"); + */ + Component.components.push(component); + } + + /** + * Component.addMachine(idMachine) + * + * @param {string} idMachine + */ + static addMachine(idMachine) + { + Component.machines[idMachine] = {}; + } + + /** + * Component.addMachineResource(idMachine, sName, data) + * + * @param {string} idMachine + * @param {string|null} sName (name of the resource) + * @param {*} data + */ + static addMachineResource(idMachine, sName, data) + { + /* + * I used to assert(Component.machines[idMachine]), but when we're running as a Node app, embed.js is not used, + * so addMachine() is never called, so resources do not need to be recorded. + */ + if (Component.machines[idMachine] && sName) { + Component.machines[idMachine][sName] = data; + } + } + + /** + * Component.getMachineResources(idMachine) + * + * @param {string} idMachine + * @return {Object|undefined} + */ + static getMachineResources(idMachine) + { + return Component.machines[idMachine]; + } + + /** + * Component.getTime() + * + * @return {number} the current time, in milliseconds + */ + static getTime() + { + return Date.now() || +new Date(); + } + + /** + * Component.log(s, type) + * + * For diagnostic output only. + * + * @param {string} [s] is the message text + * @param {string} [type] is the message type + */ + static log(s, type) + { + if (!COMPILED) { + if (s) { + var sElapsed = "", sMsg = (type? (type + ": ") : "") + s; + if (typeof Usr != "undefined") { + if (Component.msStart === undefined) { + Component.msStart = Component.getTime(); + } + sElapsed = (Component.getTime() - Component.msStart) + "ms: "; + } + if (window && window.console) console.log(sElapsed + sMsg.replace(/\n/g, " ")); + } + } + } + + /** + * Component.assert(f, s) + * + * Verifies conditions that must be true (for DEBUG builds only). + * + * The Closure Compiler should automatically remove all references to Component.assert() in non-DEBUG builds. + * TODO: Add a task to the build process that "asserts" there are no instances of "assertion failure" in RELEASE builds. + * + * @param {boolean} f is the expression we are asserting to be true + * @param {string} [s] is description of the assertion on failure + */ + static assert(f, s) + { + if (DEBUG) { + if (!f) { + if (!s) s = "assertion failure"; + Component.log(s); + throw new Error(s); + } + } + } + + /** + * Component.println(s, type, id) + * + * For non-diagnostic messages, which components may override to control the destination/appearance of their output. + * + * Components that inherit from this class should use the instance method, this.println(), rather than Component.println(), + * because if a Control Panel is loaded, it will override only the instance method, not the class method (overriding the class + * method would improperly affect any other machines loaded on the same page). + * + * @param {string} [s] is the message text + * @param {string} [type] is the message type + * @param {string} [id] is the caller's ID, if any + */ + static println(s, type, id) + { + if (!COMPILED) { + Component.log((id? (id + ": ") : "") + (s? ("\"" + s + "\"") : ""), type); + } + } + + /** + * Component.notice(s, fPrintOnly, id) + * + * notice() is like println() but implies a need for user notification, so we alert() as well. + * + * @param {string} s is the message text + * @param {boolean} [fPrintOnly] + * @param {string} [id] is the caller's ID, if any + */ + static notice(s, fPrintOnly, id) + { + if (!COMPILED) { + Component.println(s, "notice", id); + } + if (!fPrintOnly) Component.alertUser((id? (id + ": ") : "") + s); + } + + /** + * Component.warning(s) + * + * @param {string} s describes the warning + */ + static warning(s) + { + if (!COMPILED) { + Component.println(s, "warning"); + } + Component.alertUser(s); + } + + /** + * Component.error(s) + * + * @param {string} s describes the error; an alert() is displayed as well + */ + static error(s) + { + if (!COMPILED) { + Component.println(s, "error"); + } + Component.alertUser(s); + } + + /** + * Component.alertUser(sMessage) + * + * @param {string} sMessage + */ + static alertUser(sMessage) + { + if (window) { + window.alert(sMessage); + } else { + Component.log(sMessage); + } + }; + + /** + * Component.confirmUser(sPrompt) + * + * @param {string} sPrompt + * @returns {boolean} true if the user clicked OK, false if Cancel/Close + */ + static confirmUser(sPrompt) + { + var fResponse = false; + if (window) { + fResponse = window.confirm(sPrompt); + } + return fResponse; + } + + /** + * Component.promptUser() + * + * @param {string} sPrompt + * @param {string} [sDefault] + * @returns {string|null} + */ + static promptUser(sPrompt, sDefault) + { + var sResponse = null; + if (window) { + sResponse = window.prompt(sPrompt, sDefault === undefined? "" : sDefault); + } + return sResponse; + } + + /** + * Component.getComponents(idRelated) + * + * We could store components as properties, using the component's ID, and change + * this linear lookup into a property lookup, but some components may have no ID. + * + * @param {string} [idRelated] of related component + * @return {Array} of components + */ + static getComponents(idRelated) + { + var i; + var aComponents = []; + /* + * getComponentByID(id, idRelated) + * + * If idRelated is provided, we check it for a machine prefix, and use any + * existing prefix to constrain matches to IDs with the same prefix, in order to + * avoid matching components belonging to other machines. + */ + if (idRelated) { + if ((i = idRelated.indexOf('.')) > 0) + idRelated = idRelated.substr(0, i + 1); + else + idRelated = ""; + } + for (i = 0; i < Component.components.length; i++) { + var component = Component.components[i]; + if (!idRelated || !component.id.indexOf(idRelated)) { + aComponents.push(component); + } + } + return aComponents; + } + + /** + * Component.getComponentByID(id, idRelated) + * + * We could store components as properties, using the component's ID, and change + * this linear lookup into a property lookup, but some components may have no ID. + * + * @param {string} id of the desired component + * @param {string} [idRelated] of related component + * @return {Component|null} + */ + static getComponentByID(id, idRelated) + { + if (id !== undefined) { + var i; + /* + * If idRelated is provided, we check it for a machine prefix, and use any + * existing prefix to constrain matches to IDs with the same prefix, in order to + * avoid matching components belonging to other machines. + */ + if (idRelated && (i = idRelated.indexOf('.')) > 0) { + id = idRelated.substr(0, i + 1) + id; + } + for (i = 0; i < Component.components.length; i++) { + if (Component.components[i].id === id) { + return Component.components[i]; + } + } + if (Component.components.length) { + Component.log("Component ID '" + id + "' not found", "warning"); + } + } + return null; + } + + /** + * Component.getComponentByType(sType, idRelated, componentPrev) + * + * @param {string} sType of the desired component + * @param {string} [idRelated] of related component + * @param {Component|null} [componentPrev] of previously returned component, if any + * @return {Component|null} + */ + static getComponentByType(sType, idRelated, componentPrev) + { + if (sType !== undefined) { + var i; + /* + * If idRelated is provided, we check it for a machine prefix, and use any + * existing prefix to constrain matches to IDs with the same prefix, in order to + * avoid matching components belonging to other machines. + */ + if (idRelated) { + if ((i = idRelated.indexOf('.')) > 0) { + idRelated = idRelated.substr(0, i + 1); + } else { + idRelated = ""; + } + } + for (i = 0; i < Component.components.length; i++) { + if (componentPrev) { + if (componentPrev == Component.components[i]) componentPrev = null; + continue; + } + if (sType == Component.components[i].type && (!idRelated || !Component.components[i].id.indexOf(idRelated))) { + return Component.components[i]; + } + } + Component.log("Component type '" + sType + "' not found", "warning"); + } + return null; + } + + /** + * Component.getComponentParms(element) + * + * @param {Object} element from the DOM + */ + static getComponentParms(element) + { + var parms = null; + var sParms = element.getAttribute("data-value"); + if (sParms) { + try { + parms = eval("(" + sParms + ")"); // jshint ignore:line + /* + * We can no longer invoke removeAttribute() because some components (eg, Panel) need + * to run their initXXX() code more than once, to avoid initialization-order dependencies. + * + * if (!DEBUG) { + * element.removeAttribute("data-value"); + * } + */ + } catch(e) { + Component.error(e.message + " (" + sParms + ")"); + } + } + return parms; + } + + /** + * Component.bindExternalControl(component, sControl, sBinding, sType) + * + * @param {Component} component + * @param {string} sControl + * @param {string} sBinding + * @param {string} [sType] is the external component type + */ + static bindExternalControl(component, sControl, sBinding, sType) + { + if (sControl) { + if (sType === undefined) sType = "Panel"; + var target = Component.getComponentByType(sType, component.id); + if (target) { + var eBinding = target.bindings[sControl]; + if (eBinding) { + component.setBinding(null, sBinding, eBinding); + } + } + } + } + + /** + * Component.bindComponentControls(component, element, sAppClass) + * + * @param {Component} component + * @param {Object} element from the DOM + * @param {string} sAppClass + */ + static bindComponentControls(component, element, sAppClass) + { + var aeControls = Component.getElementsByClass(element.parentNode, sAppClass + "-control"); + + for (var iControl = 0; iControl < aeControls.length; iControl++) { + + var aeChildNodes = aeControls[iControl].childNodes; + + for (var iNode = 0; iNode < aeChildNodes.length; iNode++) { + var control = aeChildNodes[iNode]; + if (control.nodeType !== 1 /* document.ELEMENT_NODE */) { + continue; + } + var sClass = control.getAttribute("class"); + if (!sClass) continue; + var aClasses = sClass.split(" "); + for (var iClass = 0; iClass < aClasses.length; iClass++) { + var parms; + sClass = aClasses[iClass]; + switch (sClass) { + case sAppClass + "-binding": + parms = Component.getComponentParms(control); + if (parms && parms['binding']) { + component.setBinding(parms['type'], parms['binding'], control, parms['value']); + } else if (!parms || parms['type'] != "description") { + Component.log("Component '" + component.toString() + "' missing binding" + (parms? " for " + parms['type'] : ""), "warning"); + } + iClass = aClasses.length; + break; + default: + // if (DEBUG) Component.log("Component.bindComponentControls(" + component.toString() + "): unrecognized control class \"" + sClass + "\"", "warning"); + break; + } + } + } + } + } + + /** + * Component.getElementsByClass(element, sClass, sObjClass) + * + * This is a cross-browser helper function, since not all browser's support getElementsByClassName() + * + * TODO: This should probably be moved into weblib.js at some point, along with the control binding functions above, + * to keep all the browser-related code together. + * + * @param {Object} element from the DOM + * @param {string} sClass + * @param {string} [sObjClass] + * @return {Array|NodeList} + */ + static getElementsByClass(element, sClass, sObjClass) + { + if (sObjClass) sClass += '-' + sObjClass + "-object"; + /* + * Use the browser's built-in getElementsByClassName() if it appears to be available + * (for example, it's not available in IE8, but it should be available in IE9 and up) + */ + if (element.getElementsByClassName) { + return element.getElementsByClassName(sClass); + } + var i, j, ae = []; + var aeAll = element.getElementsByTagName("*"); + var re = new RegExp('(^| )' + sClass + '( |$)'); + for (i = 0, j = aeAll.length; i < j; i++) { + if (re.test(aeAll[i].className)) { + ae.push(aeAll[i]); + } + } + if (!ae.length) { + Component.log('No elements of class "' + sClass + '" found'); + } + return ae; + } + + /** + * Component.getScriptCommands(sScript) + * + * This is a simple parser that breaks sScript into an array of commands, where each command + * is an array of tokens, where tokens are sequences of characters separated by any of: tab, space, + * carriage-return (CR), line-feed (LF), semicolon, single-quote, or double-quote; if a quote is + * used, all characters up to the next matching quote become part of the token, allowing any of the + * other separators to be part of the token. CR, LF and semicolon also serve to terminate a command, + * with semicolon being preferred, because it's 1) more visible, and 2) essential when the entire + * script is a multi-line string where all CR/LF were replaced by spaces (which is what Jekyll does, + * and since we can't change Jekyll, it's what our own MarkDown Front Matter parser does as well; + * see convertMD() in markout.js, where the aCommandDefs array is built). + * + * Backslash sequences like \n, \r, and \\ have already been converted to LF, CR and backslash + * characters, since the entire script string is injected into a JavaScript function call, so any + * backslash sequence that JavaScript supports is automatically converted: + * + * \0 \' \" \\ \n \r \v \t \b \f \uXXXX \xXX + * ^J ^M ^K ^I ^H ^L + * + * To support any other non-printable 8-bit character, such as ESC, you should use \xXX, where XX + * is the ASCII code in hex. For ESC, that would \x1B. + * + * @param {string} sScript + * @return {Array} + */ + static getScriptCommands(sScript) + { + var cch = sScript.length; + var aCommands = [], aTokens = [], sToken = "", chQuote = null; + for (var i = 0; i < cch; i++) { + var ch = sScript[i]; + if (ch == '"' || ch == "'") { + if (chQuote && ch != chQuote) { + sToken += ch; + continue; + } + if (!chQuote) { + chQuote = ch; + } else { + chQuote = null; + } + if (sToken) { + aTokens.push(sToken); + sToken = ""; + } + continue; + } + if (!chQuote) { + if (ch == '\r' || ch == '\n') { + ch = ';'; + } + if (ch == ' ' || ch == '\t' || ch == ';') { + if (sToken) { + aTokens.push(sToken); + sToken = ""; + } + if (ch == ';' && aTokens.length) { + aCommands.push(aTokens); + aTokens = []; + } + continue; + } + } + sToken += ch; + } + if (sToken) { + aTokens.push(sToken); + } + if (aTokens.length) { + aCommands.push(aTokens); + } + return aCommands; + } + + /** + * Component.processScript(idMachine, sScript) + * + * @param {string} idMachine + * @param {string} sScript + * @return {boolean} + */ + static processScript(idMachine, sScript) + { + var fSuccess = false; + idMachine += ".machine"; + if (typeof sScript == "string" && !Component.commands[idMachine]) { + fSuccess = true; + Component.commands[idMachine] = Component.getScriptCommands(sScript); + if (!Component.processCommands(idMachine)) { + fSuccess = false; + } + } + return fSuccess; + } + + /** + * Component.processCommands(idMachine) + * + * @param {string} idMachine + * @return {boolean} + */ + static processCommands(idMachine) + { + var fSuccess = true; + var aCommands = Component.commands[idMachine]; + + // var dbg = Component.getComponentByType("Debugger", idMachine); + + while (aCommands && aCommands.length) { + + var aTokens = aCommands.splice(0, 1)[0]; + var sCommand = aTokens[0]; + + /* + * It's possible to route this output to the Debugger window with dbg.println() + * instead, but it's a bit too confusing mingling script output in a window that + * already mingles Debugger and machine output. + */ + Component.println('script: ' + aTokens.join(' ')); + + var fnCallReady = null; + if (Component.asyncCommands.indexOf(sCommand) >= 0) { + fnCallReady = function processNextCommand() { + return function() { + Component.processCommands(idMachine); + } + }(); + } + + var fnCommand = Component.globalCommands[sCommand]; + if (fnCommand) { + if (!fnCallReady) { + fSuccess = fnCommand(aTokens[1], aTokens[2], aTokens[3]); + } else { + if (!fnCommand(fnCallReady, aTokens[1], aTokens[2], aTokens[3])) break; + } + } + else { + fSuccess = false; + var component = Component.getComponentByType(aTokens[1], idMachine); + if (component) { + fnCommand = Component.componentCommands[sCommand]; + if (fnCommand) { + fSuccess = fnCommand(component, aTokens[2], aTokens[3]); + } + else { + var exports = component['exports']; + if (exports) { + fnCommand = exports[sCommand]; + if (fnCommand) { + fSuccess = true; + if (!fnCallReady) { + fSuccess = fnCommand.call(component, aTokens[2], aTokens[3]); + } else { + if (!fnCommand.call(component, fnCallReady, aTokens[2], aTokens[3])) break; + } + } + } + } + } + } + + if (!fSuccess) { + Component.alertUser("Script error: " + sCommand + (fnCommand? " failed" : " unrecognized")); + break; + } + } + + if (aCommands && !aCommands.length) { + delete Component.commands[idMachine]; + } + + return fSuccess; + } + + /** + * Component.scriptAlert(sMessage) + * + * @param {string} sMessage + * @return {boolean} + */ + static scriptAlert(sMessage) + { + Component.alertUser(sMessage); + return true; + } + + /** + * Component.scriptSelect(component, sBinding, sValue) + * + * @param {Component} component + * @param {string} sBinding + * @param {string} sValue + * @return {boolean} + */ + static scriptSelect(component, sBinding, sValue) + { + var fSuccess = false; + var aBindings = component['bindings']; + var control = aBindings[sBinding]; + if (control) { + for (var i = 0; i < control.options.length; i++) { + if (control.options[i].textContent == sValue) { + if (control.selectedIndex != i) { + control.selectedIndex = i; + } + fSuccess = true; + break; + } + } + } + return fSuccess; + } + + /** + * Component.scriptSleep(fnCallback, sDelay) + * + * @param {function()} fnCallback + * @param {string} sDelay (in milliseconds) + * @return {boolean} + */ + static scriptSleep(fnCallback, sDelay) + { + setTimeout(fnCallback, +sDelay); + return false; + } + + /** + * toString() + * + * @this {Component} + * @return {string} + */ + toString() { + return (this.name? this.name : (this.id || this.type)); + } + + /** + * getMachineNum() + * + * @this {Component} + * @return {number} unique machine number + */ + getMachineNum() { + var nMachine = 1; + if (this.idMachine) { + var aDigits = this.idMachine.match(/\d+/); + if (aDigits !== null) + nMachine = parseInt(aDigits[0], 10); + } + return nMachine; + } + + /** + * setBinding(sHTMLType, sBinding, control, sValue) + * + * Component's setBinding() method is intended to be overridden by subclasses. + * + * @this {Component} + * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") + * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "reset") + * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) + * @param {string} [sValue] optional data value + * @return {boolean} true if binding was successful, false if unrecognized binding request + */ + setBinding(sHTMLType, sBinding, control, sValue) { + switch (sBinding) { + case "clear": + if (!this.bindings[sBinding]) { + this.bindings[sBinding] = control; + control.onclick = (function(component) { + return function clearPanel() { + if (component.bindings['print']) { + component.bindings['print'].value = ""; + } + }; + }(this)); + } + return true; + case "print": + if (!this.bindings[sBinding]) { + this.bindings[sBinding] = control; + /* + * HACK: Save this particular HTML element so that the Debugger can access it, too + */ + this.controlPrint = control; + /* + * This was added for Firefox (Safari automatically clears the