From c26e4f8a4352f463531e41952f93698367d5a964 Mon Sep 17 00:00:00 2001 From: Jeff Parsons Date: Tue, 9 Apr 2019 23:11:46 -0700 Subject: [PATCH] Fixed copy-protection support in compiled version --- disks/pcx86/apps/microsoft/word/1.15/README.md | 2 +- modules/pcx86/lib/disk.js | 5 +++++ modules/pcx86/lib/fdc.js | 2 +- versions/pcx86/1.75.0/pcx86-uncompiled.js | 4 ++-- versions/pcx86/1.75.0/pcx86.js | 8 ++++---- versions/pcx86/1.75.0/pcx86.js.map | 2 +- 6 files changed, 14 insertions(+), 9 deletions(-) diff --git a/disks/pcx86/apps/microsoft/word/1.15/README.md b/disks/pcx86/apps/microsoft/word/1.15/README.md index 6e21a09511..f147ac0209 100644 --- a/disks/pcx86/apps/microsoft/word/1.15/README.md +++ b/disks/pcx86/apps/microsoft/word/1.15/README.md @@ -11,7 +11,7 @@ machines: name: PC DOS 2.00 (Disk 1) B: name: MS Word 1.15 (Program) - autoType: $date\r$time\rB:\rDIR\r + autoType: $date\r$time\rB:\rWORD\r --- Microsoft Word 1.15 diff --git a/modules/pcx86/lib/disk.js b/modules/pcx86/lib/disk.js index 249aa022b1..93e8e5c859 100644 --- a/modules/pcx86/lib/disk.js +++ b/modules/pcx86/lib/disk.js @@ -2942,6 +2942,11 @@ FileInfo.NE = { * @typedef {Object} SectorInfo * @property {number} sector * @property {number} length + * @property {number} dataMark + * @property {number} headCRC + * @property {boolean} headError + * @property {number} dataCRC + * @property {boolean} dataError * @property {Array.} data * @property {number|null} pattern * @property {number} iCylinder diff --git a/modules/pcx86/lib/fdc.js b/modules/pcx86/lib/fdc.js index c3ce6471bd..fddaef0b9f 100644 --- a/modules/pcx86/lib/fdc.js +++ b/modules/pcx86/lib/fdc.js @@ -2684,7 +2684,7 @@ class FDC extends Component { drive.resCode = FDC.REG_DATA.RES.NO_DATA | FDC.REG_DATA.RES.INCOMPLETE; break; } - if (drive.sector.dataError) { + if (drive.sector['dataError']) { drive.resCode = FDC.REG_DATA.RES.CRC_ERROR | FDC.REG_DATA.RES.INCOMPLETE; } drive.ibSector = 0; diff --git a/versions/pcx86/1.75.0/pcx86-uncompiled.js b/versions/pcx86/1.75.0/pcx86-uncompiled.js index 965a7109a9..43d78b9870 100644 --- a/versions/pcx86/1.75.0/pcx86-uncompiled.js +++ b/versions/pcx86/1.75.0/pcx86-uncompiled.js @@ -63539,7 +63539,7 @@ FileInfo.NE = { */ }; -/** @typedef {{ sector: number, length: number, data: Array., pattern: (number|null), iCylinder: number, iHead: number, iModify: number, cModify: number, file: FileInfo, offFile: number }} */ +/** @typedef {{ sector: number, length: number, dataMark: number, headCRC: number, headError: boolean, dataCRC: number, dataError: boolean, data: Array., pattern: (number|null), iCylinder: number, iHead: number, iModify: number, cModify: number, file: FileInfo, offFile: number }} */ var SectorInfo; @@ -66166,7 +66166,7 @@ class FDC extends Component { drive.resCode = FDC.REG_DATA.RES.NO_DATA | FDC.REG_DATA.RES.INCOMPLETE; break; } - if (drive.sector.dataError) { + if (drive.sector['dataError']) { drive.resCode = FDC.REG_DATA.RES.CRC_ERROR | FDC.REG_DATA.RES.INCOMPLETE; } drive.ibSector = 0; diff --git a/versions/pcx86/1.75.0/pcx86.js b/versions/pcx86/1.75.0/pcx86.js index 292d1d5498..5910027c71 100644 --- a/versions/pcx86/1.75.0/pcx86.js +++ b/versions/pcx86/1.75.0/pcx86.js @@ -46,7 +46,7 @@ https://www.pcjs.org/modules/shared/lib/save.js (C) Jeff Parsons 2012-2019 */ var m,aa,ba="function"==typeof Object.create?Object.create:function(a){function b(){}b.prototype=a;return new b},ca;if("function"==typeof Object.setPrototypeOf)ca=Object.setPrototypeOf;else{var da;a:{var ea={Cd:!0},fa={};try{fa.__proto__=ea;da=fa.Cd;break a}catch(a){}da=!1}ca=da?function(a,b){a.__proto__=b;if(a.__proto__!==b)throw new TypeError(a+" is not extensible");return a}:null}var ha=ca; -function ia(a,b){a.prototype=ba(b.prototype);a.prototype.constructor=a;if(ha)ha(a,b);else for(var c in b)if("prototype"!=c)if(Object.defineProperties){var d=Object.getOwnPropertyDescriptor(b,c);d&&Object.defineProperty(a,c,d)}else a[c]=b[c];a.Kq=b.prototype} +function ia(a,b){a.prototype=ba(b.prototype);a.prototype.constructor=a;if(ha)ha(a,b);else for(var c in b)if("prototype"!=c)if(Object.defineProperties){var d=Object.getOwnPropertyDescriptor(b,c);d&&Object.defineProperty(a,c,d)}else a[c]=b[c];a.Jq=b.prototype} var ja="function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){a!=Array.prototype&&a!=Object.prototype&&(a[b]=c.value)},ka="undefined"!=typeof window&&window===this?this:"undefined"!=typeof global&&null!=global?global:this;function ma(){ma=function(){};ka.Symbol||(ka.Symbol=na)}var na=function(){var a=0;return function(b){return"jscomp_symbol_"+(b||"")+a++}}(); function oa(){ma();var a=ka.Symbol.iterator;a||(a=ka.Symbol.iterator=ka.Symbol("iterator"));"function"!=typeof Array.prototype[a]&&ja(Array.prototype,a,{configurable:!0,writable:!0,value:function(){return pa(this)}});oa=function(){}}function pa(a){var b=0;return qa(function(){return b=f;f++){var g="pcjs-bitCell";f||(g+=" pcjs-bitCellLeft");d+='\x3cdiv id\x3d"'+(c+"-"+f)+'" class\x3d"'+g+'" data-value\x3d"0"\x3e'+f+"\x3c/div\x3e\n"}e.innerHTML=d;Fo(a,b,c,!0)}function Go(a,b,c){if(b=(a=Ho[a.ba|0])&&a[b])for(var d in b)if(a=b[d],a.Cc&1<e;e++){if(128==xy(a,d,b,1)){c.lg=xy(a,d,b+8,4);(d=wy(a,c.lg))&&xy(a,d,11,2)==a.Oa&&(f=!0);break}b+=16}if(!f)return}c.Tf||(c.Tf=xy(a,d,19,2)||xy(a,d,32,4),c.Sf=xy(a,d,14,2),c.Rh=c.Sf+xy(a,d,22,2)*xy(a,d,16,1),c.bi=xy(a,d,17,2),c.Yf=xy(a,d,13,1));c.Ph=c.Rh+((32*c.bi+(c.Oa-1))/c.Oa|0);c.so=(c.Tf-c.Ph)/c.Yf|0;c.$g=4084>=c.so?12:16;c.Vm=12==c.$g?4086:65526;e=[];for(d=c.Rh;d>=8;f+=2;if(l)for(;k--;)Ly(c,f,1),254>=l?(n=l,t=Ly(c,f+1),f+=3):(n=Ly(c,f+3,1),t=Ly(c,f+4),f+=6),c.Ce[n]&&(c.Ce[n].Gf[h]=[t]),c.A[h]=[n,t],h++;else h+=k}(g=Ay(d,Oy,b))&&Py(d,g+b);g=Ay(d,Qy,b);h=Ay(d,Ry,b);g&&h&&Py(d,g,g+h)}}}} -function zy(a,b,c,d,e){var f=a.C.length,g=b.Oa/32|0;b.Jq=d+"\\";for(var h=0;hC)break;for(var G=q.Ph+(C-2)*q.Yf,B=0;BC)break;for(var G=q.Ph+(C-2)*q.Yf,B=0;B>3,1),d?e=16==b.$g?e<<8:c&7?e<<4:(e&15)<<8:c&7&&(e>>=4));return e} function wy(a,b){var c=a.fb*a.ab,d=b/c|0;return dg)break;e|=g<=f)break;e+=String.fromCharCode(f)}return e}function qy(a,b,c,d,e,f){a||(a={sector:d,length:e,data:[],pattern:f});a.Wm=b;a.Xm=c;a.td=a.Tc=0;a.Mg=!1;return a} function sy(a,b){b="action\x3dopen\x26volume\x3d"+b+("\x26mode\x3d"+a.mode);b+="\x26chs\x3d"+a.vb+":"+a.fb+":"+a.ab+":"+a.Oa;b+="\x26machine\x3d"+oy(a.controller);b+="\x26user\x3d"+py(a.controller);return fb()+"/api/v1/disk?"+b} @@ -769,7 +769,7 @@ g),dp(this.W,2))}g=!0}d=g;Qz(this,f,a,b,e,h,k,l);break;case Rz:e=Ez(this);this.Z e=f.Bb;h=f.Ta=b;k=1;l=Ez(this);f.xb=128<>2&1,e=Ez(this),f.Bb+=e-f.ie,0>f.Bb&&(f.Bb=0),f.Bb>=f.vb&&(f.Bb=f.vb-1),f.ie=e,f.gb=Sz,f.Bb||(f.gb|=Tz),Gz(this),d=!0}0>>8);Iz(a,(b.gb&$z)>>>16);var k=0;if(e!=b.Bb||f!=b.Ta)k=g=1;c&aA&&(f^=k,d||(k=0));Iz(a,e+k);Iz(a,f);Iz(a,g);Iz(a,h)}function Ez(a){var b=a.G[a.D];a.D++;return b}function Gz(a){a.Bf();a.D=a.C=0}function Iz(a,b){a.G[a.C++]=b} m.em=function(a,b,c){void 0===b||0>b?this.Pe(a,c):c(-1,!1)};m.fm=function(a,b){return void 0!==b&&0<=b?bA(a,b):-1};m.Om=function(a,b){if(void 0!==b&&0<=b)a:if(a.gb)a=-1;else{a.cd[a.gf++]=b;if(a.gf==a.cd.length){a.Bb=a.cd[0];a.Ta=a.cd[1];a.jb=a.cd[2];a.xb=128<bA(a,a.hj)){a=-1;break a}a.Dg++}a.Dg>=a.je&&(b=-1);a=b}else a=-1;return a}; -m.Pe=function(a,b){var c=-1,d=null,e=0;if((!a.gb||a.gb==(cA|Nz))&&a.va){do{if(a.hb&&(e=a.$a,0<=(c=a.va.read(a.hb,a.$a++)))){d=a.hb;break}a.hb=a.va.seek(a.Bb,a.Ta,a.jb);if(!a.hb){a.gb=Oz|Nz;break}a.hb.Hq&&(a.gb=cA|Nz);a.$a=0;dA(a)}while(1)}b(c,!1,d,e)};function bA(a,b){if(a.gb||!a.va)return-1;do{if(a.hb&&a.va.write(a.hb,a.$a++,b))break;a.hb=a.va.seek(a.Bb,a.Ta,a.jb);if(!a.hb){a.gb=cA|Nz;b=-1;break}a.$a=0;dA(a)}while(1);return b} +m.Pe=function(a,b){var c=-1,d=null,e=0;if((!a.gb||a.gb==(cA|Nz))&&a.va){do{if(a.hb&&(e=a.$a,0<=(c=a.va.read(a.hb,a.$a++)))){d=a.hb;break}a.hb=a.va.seek(a.Bb,a.Ta,a.jb);if(!a.hb){a.gb=Oz|Nz;break}a.hb.dataError&&(a.gb=cA|Nz);a.$a=0;dA(a)}while(1)}b(c,!1,d,e)};function bA(a,b){if(a.gb||!a.va)return-1;do{if(a.hb&&a.va.write(a.hb,a.$a++,b))break;a.hb=a.va.seek(a.Bb,a.Ta,a.jb);if(!a.hb){a.gb=cA|Nz;b=-1;break}a.$a=0;dA(a)}while(1);return b} function dA(a){a.jb++;a.jb>=a.ag+1&&(a.jb=1,a.Ta++,a.Ta>=a.$f&&(a.Ta=0,a.Bb++))}var qz="Floppy Drive",yz=4,zz=8,Bz=16,Az=64,oz=128,Fz=3,Hz=4,Kz=5,Lz=6,Rz=7,Uz=8,Wz=10,Xz=13,Yz=15,Cz=31,aA=128,vz=0,Mz=8,Sz=32,Nz=64,pz=192,Vz=255,Pz=512,Oz=1024,cA=8192,Zz=65280,$z=16711680,Tz=268435456,Jz=-16777216,xz=128,tz=0;aa={}; var Dz={3:{$d:3,ke:0,name:aa.Bq},4:{$d:2,ke:1,name:aa.zq},5:{$d:9,ke:7,name:aa.Fq},6:{$d:9,ke:7,name:aa.vq},7:{$d:2,ke:0,name:aa.xq},8:{$d:1,ke:2,name:aa.Aq},10:{$d:2,ke:7,name:aa.wq},13:{$d:6,ke:7,name:aa.sq},15:{$d:3,ke:0,name:aa.yq}},hz={1009:bz.prototype.En,1012:bz.prototype.Gn,1013:bz.prototype.Dn,1015:bz.prototype.Fn},iz={1010:bz.prototype.ep,1013:bz.prototype.cp,1015:bz.prototype.bp}; xb(function(){for(var a=Xb(document,"pcx86","fdc"),b=0;b\";\n\nvar LICENSE = \"License: GPL version 3 or later \";\n\nvar CSSCLASS = \"pcjs\";\n\n/**\n * @define {string}\n */\nvar SITEURL = \"http://localhost:8088\"; // this @define is overridden by the Closure Compiler with \"https://www.pcjs.org\"\n\n/**\n * @define {boolean}\n */\nvar COMPILED = false; // this @define is overridden by the Closure Compiler (to true)\n\n/**\n * @define {boolean}\n */\nvar DEBUG = true; // this @define is overridden by the Closure Compiler (to false) to remove DEBUG-only code\n\n/**\n * @define {boolean}\n */\nvar MAXDEBUG = false; // this @define is overridden by the Closure Compiler (to false) to remove MAXDEBUG-only code\n\n/**\n * @define {boolean}\n */\nvar PRIVATE = false; // this @define is overridden by the Closure Compiler (to false) to enable PRIVATE code\n\n/*\n * RS-232 DB-25 Pin Definitions, mapped to bits 1-25 in a 32-bit status value.\n *\n * SerialPorts in PCjs machines are considered DTE (Data Terminal Equipment), which means they should be \"virtually\"\n * connected to each other via a null-modem cable, which assumes the following cross-wiring:\n *\n * G 1 <-> 1 G (Ground)\n * TD 2 <-> 3 RD (Received Data)\n * RD 3 <-> 2 TD (Transmitted Data)\n * RTS 4 <-> 5 CTS (Clear To Send)\n * CTS 5 <-> 4 RTS (Request To Send)\n * DSR 6+8 <-> 20 DTR (Data Terminal Ready)\n * SG 7 <-> 7 SG (Signal Ground)\n * DTR 20 <-> 6+8 DSR (Data Set Ready + Carrier Detect)\n * RI 22 <-> 22 RI (Ring Indicator)\n *\n * TODO: Move these definitions to a more appropriate shared file at some point.\n */\nvar RS232 = {\n RTS: {\n PIN: 4,\n MASK: 0x00000010\n },\n CTS: {\n PIN: 5,\n MASK: 0x00000020\n },\n DSR: {\n PIN: 6,\n MASK: 0x00000040\n },\n CD: {\n PIN: 8,\n MASK: 0x00000100\n },\n DTR: {\n PIN: 20,\n MASK: 0x00100000\n },\n RI: {\n PIN: 22,\n MASK: 0x00400000\n }\n};\n\n/*\n * NODE should be true if we're running under NodeJS (eg, command-line), false if not (eg, web browser)\n */\nvar NODE = false;\n\n\n/**\n * @copyright https://www.pcjs.org/modules/shared/lib/diskapi.js (C) Jeff Parsons 2012-2019\n */\n\n/*\n * Our \"DiskIO API\" looks like:\n *\n * http://www.pcjs.org/api/v1/disk?action=open&volume=*10mb.img&mode=demandrw&chs=c:h:s&machine=xxx&user=yyy\n */\nvar DiskAPI = {\n ENDPOINT: \"/api/v1/disk\",\n QUERY: {\n ACTION: \"action\", // value is one of DiskAPI.ACTION.*\n VOLUME: \"volume\", // value is path of a disk image\n MODE: \"mode\", // value is one of DiskAPI.MODE.*\n CHS: \"chs\", // value is cylinders:heads:sectors:bytes\n ADDR: \"addr\", // value is cylinder:head:sector:count\n MACHINE: \"machine\", // value is machine token\n USER: \"user\", // value is user ID\n DATA: \"data\" // value is data to be written\n },\n ACTION: {\n OPEN: \"open\",\n READ: \"read\",\n WRITE: \"write\",\n CLOSE: \"close\"\n },\n MODE: {\n LOCAL: \"local\", // this mode implies no API (at best, localStorage backing only)\n PRELOAD: \"preload\", // this mode implies use of the DumpAPI\n DEMANDRW: \"demandrw\",\n DEMANDRO: \"demandro\"\n },\n FAIL: {\n BADACTION: \"invalid action\",\n BADUSER: \"invalid user\",\n BADVOL: \"invalid volume\",\n OPENVOL: \"unable to open volume\",\n CREATEVOL: \"unable to create volume\",\n WRITEVOL: \"unable to write volume\",\n REVOKED: \"access revoked\"\n }\n};\n\n/*\n * TODO: Eventually, our tools will need to support looking up disk formats by \"model\" rather than by raw disk size,\n * because obviously multiple disk geometries can yield the same raw disk size. For each conflict that arises, I'll\n * probably create a fake (approximate) disk size entry above, and then create a mapping to that approximate size below.\n */\nDiskAPI.MODELS = {\n \"RL01\": 5242880,\n \"RL02\": 10485760\n};\n\nDiskAPI.MBR = {\n PARTITIONS: {\n OFFSET: 0x1BE,\n ENTRY: {\n STATUS: 0x00, // 1-byte (0x80 if active)\n CHS_FIRST: 0x01, // 3-byte CHS specifier\n TYPE: 0x04, // 1-byte TYPE (see below)\n CHS_LAST: 0x05, // 3-byte CHS specifier\n LBA_FIRST: 0x08, // 4-byte Logical Block Address\n LBA_TOTAL: 0x0C, // 4-byte Logical Block Address\n },\n ENTRY_LENGTH: 0x10,\n STATUS: {\n ACTIVE: 0x80\n },\n TYPE: {\n EMPTY: 0x00,\n FAT12_PRIMARY: 0x01, // DOS 2.0 and up (12-bit FAT)\n FAT16_PRIMARY: 0x04 // DOS 3.0 and up (16-bit FAT)\n }\n },\n SIG_OFFSET: 0x1FE,\n SIGNATURE: 0xAA55 // to be clear, the low byte (at offset 0x1FE) is 0x55 and the high byte (at offset 0x1FF) is 0xAA\n};\n\n/*\n * Boot sector offsets (and assorted constants) in DOS-compatible boot sectors (DOS 2.0 and up)\n *\n * WARNING: I've heard apocryphal stories about SIGNATURE being improperly reversed on some systems\n * (ie, 0x55AA instead 0xAA55) -- perhaps by a dyslexic programmer -- so be careful out there.\n */\nDiskAPI.BOOT = {\n JMP_OPCODE: 0x000, // 1 byte for a JMP opcode, followed by a 1 or 2-byte offset\n OEM_STRING: 0x003, // 8 bytes\n SIG_OFFSET: 0x1FE,\n SIGNATURE: 0xAA55 // to be clear, the low byte (at offset 0x1FE) is 0x55 and the high byte (at offset 0x1FF) is 0xAA\n};\n\n/*\n * BIOS Parameter Block (BPB) offsets in DOS-compatible boot sectors (DOS 2.x and up)\n *\n * NOTE: DOS 2.x OEM documentation says that the words starting at offset 0x018 (TRACK_SECS, TOTAL_HEADS, and HIDDEN_SECS)\n * are optional, but even the DOS 2.0 FORMAT utility initializes all three of those words. There may be some OEM media out\n * there with BPBs that are only valid up to offset 0x018, but I've not run across any media like that.\n *\n * DOS 3.20 added LARGE_SECS, but unfortunately, it was added as a 2-byte value at offset 0x01E. DOS 3.31 decided\n * to make both HIDDEN_SECS and LARGE_SECS 4-byte values, which meant that LARGE_SECS had to move from 0x01E to 0x020.\n */\nDiskAPI.BPB = {\n SECTOR_BYTES: 0x00B, // 2 bytes: bytes per sector (eg, 0x200 or 512)\n CLUSTER_SECS: 0x00D, // 1 byte: sectors per cluster (eg, 1)\n RESERVED_SECS: 0x00E, // 2 bytes: reserved sectors; ie, # sectors preceding the first FAT--usually just the boot sector (eg, 1)\n TOTAL_FATS: 0x010, // 1 byte: FAT copies (eg, 2)\n ROOT_DIRENTS: 0x011, // 2 bytes: root directory entries (eg, 0x40 or 64) 0x40 * 0x20 = 0x800 (1 sector is 0x200 bytes, total of 4 sectors)\n TOTAL_SECS: 0x013, // 2 bytes: number of sectors (eg, 0x140 or 320); if zero, refer to LARGE_SECS\n MEDIA_ID: 0x015, // 1 byte: media ID (see DiskAPI.FAT.MEDIA_*); should also match the first byte of the FAT (aka FAT ID)\n FAT_SECS: 0x016, // 2 bytes: sectors per FAT (eg, 1)\n TRACK_SECS: 0x018, // 2 bytes: sectors per track (eg, 8)\n TOTAL_HEADS: 0x01A, // 2 bytes: number of heads (eg, 1)\n HIDDEN_SECS: 0x01C, // 2 bytes (DOS 2.x) or 4 bytes (DOS 3.31 and up): number of hidden sectors (always 0 for non-partitioned media)\n LARGE_SECS: 0x020 // 4 bytes (DOS 3.31 and up): number of sectors if TOTAL_SECS is zero\n};\n\n/*\n * Common (supported) diskette geometries.\n *\n * Each entry in GEOMETRIES is an array of values in \"CHS\" order:\n *\n * [# cylinders, # heads, # sectors/track, # bytes/sector, media ID]\n *\n * If the 4th value is omitted, the sector size is assumed to be 512. The order of these \"geometric\" values mirrors\n * the structure of our JSON-encoded disk images, which consist of an array of cylinders, each of which is an array of\n * heads, each of which is an array of sector objects.\n */\nDiskAPI.GEOMETRIES = {\n 163840: [40,1,8,,0xFE], // media ID 0xFE: 40 cylinders, 1 head (single-sided), 8 sectors/track, ( 320 total sectors x 512 bytes/sector == 163840)\n 184320: [40,1,9,,0xFC], // media ID 0xFC: 40 cylinders, 1 head (single-sided), 9 sectors/track, ( 360 total sectors x 512 bytes/sector == 184320)\n 327680: [40,2,8,,0xFF], // media ID 0xFF: 40 cylinders, 2 heads (double-sided), 8 sectors/track, ( 640 total sectors x 512 bytes/sector == 327680)\n 368640: [40,2,9,,0xFD], // media ID 0xFD: 40 cylinders, 2 heads (double-sided), 9 sectors/track, ( 720 total sectors x 512 bytes/sector == 368640)\n 737280: [80,2,9,,0xF9], // media ID 0xF9: 80 cylinders, 2 heads (double-sided), 9 sectors/track, (1440 total sectors x 512 bytes/sector == 737280)\n 1228800: [80,2,15,,0xF9], // media ID 0xF9: 80 cylinders, 2 heads (double-sided), 15 sectors/track, (2400 total sectors x 512 bytes/sector == 1228800)\n 1474560: [80,2,18,,0xF0], // media ID 0xF0: 80 cylinders, 2 heads (double-sided), 18 sectors/track, (2880 total sectors x 512 bytes/sector == 1474560)\n 2949120: [80,2,36,,0xF0], // media ID 0xF0: 80 cylinders, 2 heads (double-sided), 36 sectors/track, (5760 total sectors x 512 bytes/sector == 2949120)\n /*\n * The following are some common disk sizes and their CHS values, since missing or bogus MBR and/or BPB values\n * might mislead us when attempting to determine the exact disk geometry.\n */\n 10653696:[306,4,17], // PC XT 10Mb hard drive (type 3)\n 21411840:[615,4,17], // PC AT 20Mb hard drive (type 2)\n /*\n * Assorted DEC disk formats.\n */\n 256256: [77, 1,26,128], // RX01 single-platter diskette: 77 tracks, 1 head, 26 sectors/track, 128 bytes/sector, for a total of 256256 bytes\n 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\n 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\n 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\n};\n\n/*\n * Media ID (descriptor) bytes for DOS-compatible FAT-formatted disks (stored in the first byte of the FAT)\n */\nDiskAPI.FAT = {\n MEDIA_160KB: 0xFE, // 5.25-inch, 1-sided, 8-sector, 40-track\n MEDIA_180KB: 0xFC, // 5.25-inch, 1-sided, 9-sector, 40-track\n MEDIA_320KB: 0xFF, // 5.25-inch, 2-sided, 8-sector, 40-track\n MEDIA_360KB: 0xFD, // 5.25-inch, 2-sided, 9-sector, 40-track\n MEDIA_720KB: 0xF9, // 3.5-inch, 2-sided, 9-sector, 80-track\n MEDIA_1200KB: 0xF9, // 3.5-inch, 2-sided, 15-sector, 80-track\n MEDIA_FIXED: 0xF8, // fixed disk (aka hard drive)\n MEDIA_1440KB: 0xF0, // 3.5-inch, 2-sided, 18-sector, 80-track\n MEDIA_2880KB: 0xF0 // 3.5-inch, 2-sided, 36-sector, 80-track\n};\n\n/*\n * Cluster constants for 12-bit FATs (CLUSNUM_FREE, CLUSNUM_RES and CLUSNUM_MIN are the same for all FATs)\n */\nDiskAPI.FAT12 = {\n MAX_CLUSTERS: 4084,\n CLUSNUM_FREE: 0, // this should NEVER appear in cluster chain (except at the start of an empty chain)\n CLUSNUM_RES: 1, // reserved; this should NEVER appear in cluster chain\n CLUSNUM_MIN: 2, // smallest valid cluster number\n CLUSNUM_MAX: 0xFF6, // largest valid cluster number\n CLUSNUM_BAD: 0xFF7, // bad cluster; this should NEVER appear in cluster chain\n CLUSNUM_EOC: 0xFF8 // end of chain (actually, anything from 0xFF8-0xFFF indicates EOC)\n};\n\n/*\n * Cluster constants for 16-bit FATs (CLUSNUM_FREE, CLUSNUM_RES and CLUSNUM_MIN are the same for all FATs)\n */\nDiskAPI.FAT16 = {\n MAX_CLUSTERS: 65524,\n CLUSNUM_FREE: 0, // this should NEVER appear in cluster chain (except at the start of an empty chain)\n CLUSNUM_RES: 1, // reserved; this should NEVER appear in cluster chain\n CLUSNUM_MIN: 2, // smallest valid cluster number\n CLUSNUM_MAX: 0xFFF6, // largest valid cluster number\n CLUSNUM_BAD: 0xFFF7, // bad cluster; this should NEVER appear in cluster chain\n CLUSNUM_EOC: 0xFFF8 // end of chain (actually, anything from 0xFFF8-0xFFFF indicates EOC)\n};\n\n/*\n * Directory Entry offsets (and assorted constants) in FAT disk images\n *\n * NOTE: Versions of DOS prior to 2.0 used INVALID exclusively to mark available directory entries; any entry marked\n * UNUSED was actually considered USED. In DOS 2.0 and up, UNUSED was added to indicate that all remaining entries were\n * unused, relieving it from having to initialize the rest of the sectors in the directory cluster(s). And in fact,\n * you will likely encounter garbage in subsequent directory sectors if you read beyond the first UNUSED entry.\n */\nDiskAPI.DIRENT = {\n NAME: 0x000, // 8 bytes\n EXT: 0x008, // 3 bytes\n ATTR: 0x00B, // 1 byte\n MODTIME: 0x016, // 2 bytes\n MODDATE: 0x018, // 2 bytes\n CLUSTER: 0x01A, // 2 bytes\n SIZE: 0x01C, // 4 bytes (typically zero for subdirectories)\n LENGTH: 0x20, // 32 bytes total\n UNUSED: 0x00, // indicates this and all subsequent directory entries are unused\n INVALID: 0xE5 // indicates this directory entry is unused\n};\n\n/*\n * Possible values for DIRENT.ATTR\n */\nDiskAPI.ATTR = {\n READONLY: 0x01, // PC-DOS 2.0 and up\n HIDDEN: 0x02,\n SYSTEM: 0x04,\n LABEL: 0x08, // PC-DOS 2.0 and up\n SUBDIR: 0x10, // PC-DOS 2.0 and up\n ARCHIVE: 0x20 // PC-DOS 2.0 and up\n};\n\n\n\n/**\n * @copyright https://www.pcjs.org/modules/shared/lib/dumpapi.js (C) Jeff Parsons 2012-2019\n */\n\n/*\n * Our \"DiskDump API\", such as it was, used to look like:\n *\n * http://pcjs.org/bin/convdisk.php?disk=/disks/pc/dos/ibm/2.00/PCDOS200-DISK1.json&format=img\n *\n * To make it (a bit) more \"REST-like\", the above request now looks like:\n *\n * https://www.pcjs.org/api/v1/dump?disk=/disks/pc/dos/ibm/2.00/PCDOS200-DISK1.json&format=img\n *\n * Similarly, our \"FileDump API\" used to look like:\n *\n * http://pcjs.org/bin/convrom.php?rom=/devices/pc/rom/5150/1981-04-24/PCBIOS-REV1.rom&format=json\n *\n * and that request now looks like:\n *\n * https://www.pcjs.org/api/v1/dump?file=/devices/pc/rom/5150/1981-04-24/PCBIOS-REV1.rom&format=json\n *\n * I don't think it makes sense to avoid \"query\" parameters, because blending the path of a disk image with the\n * the rest of the URL would be (a) confusing, and (b) more work to parse.\n */\nvar DumpAPI = {\n ENDPOINT: \"/api/v1/dump\",\n QUERY: {\n DIR: \"dir\", // value is path of a directory (DiskDump only)\n DISK: \"disk\", // value is path of a disk image (DiskDump only)\n FILE: \"file\", // value is path of a ROM image file (FileDump only)\n IMG: \"img\", // alias for DISK\n PATH: \"path\", // value is path of a one or more files (DiskDump only)\n FORMAT: \"format\", // value is one of FORMAT values below\n COMMENTS: \"comments\", // value is either \"true\" or \"false\"\n DECIMAL: \"decimal\", // value is either \"true\" to force all numbers to decimal, \"false\" or undefined otherwise\n MBHD: \"mbhd\", // value is hard drive size in Mb (formerly \"mbsize\") (DiskDump only) (DEPRECATED)\n SIZE: \"size\" // value is target disk size in Kb (supersedes \"mbhd\") (DiskDump only)\n },\n FORMAT: {\n JSON: \"json\", // default\n JSON_GZ: \"gz\", // gzip is currently used ONLY for compressed JSON\n DATA: \"data\", // same as \"json\", but built without JSON.stringify() (DiskDump only)\n HEX: \"hex\", // deprecated\n OCTAL: \"octal\", // displays data as octal words\n BYTES: \"bytes\", // displays data as hex bytes; normally used only when comments are enabled\n WORDS: \"words\", // displays data as hex words; normally used only when comments are enabled\n LONGS: \"longs\", // displays data as dwords\n IMG: \"img\", // returns the raw disk data (ie, using a Buffer object) (DiskDump only)\n ROM: \"rom\" // returns the raw file data (ie, using a Buffer object) (FileDump only)\n }\n};\n\n/*\n * Because we use an overloaded API endpoint (ie, one that's shared with the FileDump module), we must\n * also provide a list of commands which, when combined with the endpoint, define a unique request.\n */\nDumpAPI.asDiskCommands = [DumpAPI.QUERY.DIR, DumpAPI.QUERY.DISK, DumpAPI.QUERY.PATH];\nDumpAPI.asFileCommands = [DumpAPI.QUERY.FILE];\n\n\n\n/**\n * @copyright https://www.pcjs.org/modules/shared/lib/reportapi.js (C) Jeff Parsons 2012-2019\n */\n\nvar ReportAPI = {\n ENDPOINT: \"/api/v1/report\",\n QUERY: {\n APP: \"app\",\n VER: \"ver\",\n URL: \"url\",\n USER: \"user\",\n TYPE: \"type\",\n DATA: \"data\"\n },\n TYPE: {\n BUG: \"bug\"\n },\n RES: {\n OK: \"Thank you\"\n }\n};\n\n\n\n/**\n * @copyright https://www.pcjs.org/modules/shared/lib/userapi.js (C) Jeff Parsons 2012-2019\n */\n\n/*\n * Examples of User API requests:\n *\n * web.getHostOrigin() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.VERIFY + '&' + UserAPI.QUERY.USER + '=' + sUser;\n */\nvar UserAPI = {\n ENDPOINT: \"/api/v1/user\",\n QUERY: {\n REQ: \"req\", // specifies a request\n USER: \"user\", // specifies a user ID\n STATE: \"state\", // specifies a state ID\n DATA: \"data\" // specifies state data\n },\n REQ: {\n CREATE: \"create\", // creates a user ID\n VERIFY: \"verify\", // requests verification of a user ID\n STORE: \"store\", // stores a machine state on the server\n LOAD: \"load\" // loads a machine state from the server\n },\n RES: {\n CODE: \"code\",\n DATA: \"data\"\n },\n CODE: {\n OK: \"ok\",\n FAIL: \"error\"\n },\n FAIL: {\n DUPLICATE: \"user already exists\",\n VERIFY: \"unable to verify user\",\n BADSTATE: \"invalid state parameter\",\n NOSTATE: \"no machine state\",\n BADLOAD: \"unable to load machine state\",\n BADSTORE: \"unable to save machine state\"\n }\n};\n\n\n\n/**\n * @copyright https://www.pcjs.org/modules/shared/lib/keys.js (C) Jeff Parsons 2012-2019\n */\n\nvar Keys = {\n /*\n * Keys and/or key combinations that generate common ASCII codes.\n *\n * NOTE: If you're looking for a general-purpose ASCII code table, see Str.ASCII in strlib.js;\n * if something's missing, that's probably the more appropriate table to add it to.\n *\n * TODO: The Closure Compiler doesn't inline all references to these values, at least those with\n * quoted property names, which is why I've 'unquoted' as many of them as possible. One solution\n * would be to add mnemonics for all of them, not just the non-printable ones (eg, SPACE instead\n * of ' ', AMP instead of '&', etc.)\n */\n ASCII: {\n BREAK: 0, CTRL_A: 1, CTRL_B: 2, CTRL_C: 3, CTRL_D: 4, CTRL_E: 5, CTRL_F: 6, CTRL_G: 7,\n CTRL_H: 8, CTRL_I: 9, CTRL_J: 10, CTRL_K: 11, CTRL_L: 12, CTRL_M: 13, CTRL_N: 14, CTRL_O: 15,\n CTRL_P: 16, CTRL_Q: 17, CTRL_R: 18, CTRL_S: 19, CTRL_T: 20, CTRL_U: 21, CTRL_V: 22, CTRL_W: 23,\n CTRL_X: 24, CTRL_Y: 25, CTRL_Z: 26, ESC: 27,\n ' ': 32, '!': 33, '\"': 34, '#': 35, '$': 36, '%': 37, '&': 38, \"'\": 39,\n '(': 40, ')': 41, '*': 42, '+': 43, ',': 44, '-': 45, '.': 46, '/': 47,\n '0': 48, '1': 49, '2': 50, '3': 51, '4': 52, '5': 53, '6': 54, '7': 55,\n '8': 56, '9': 57, ':': 58, ';': 59, '<': 60, '=': 61, '>': 62, '?': 63,\n '@': 64, A: 65, B: 66, C: 67, D: 68, E: 69, F: 70, G: 71,\n H: 72, I: 73, J: 74, K: 75, L: 76, M: 77, N: 78, O: 79,\n P: 80, Q: 81, R: 82, S: 83, T: 84, U: 85, V: 86, W: 87,\n X: 88, Y: 89, Z: 90, '[': 91, '\\\\':92, ']': 93, '^': 94, '_': 95,\n '`': 96, a: 97, b: 98, c: 99, d: 100, e: 101, f: 102, g: 103,\n h: 104, i: 105, j: 106, k: 107, l: 108, m: 109, n: 110, o: 111,\n p: 112, q: 113, r: 114, s: 115, t: 116, u: 117, v: 118, w: 119,\n x: 120, y: 121, z: 122, '{':123, '|':124, '}':125, '~':126, DEL: 127\n },\n /*\n * Browser keyCodes we must pay particular attention to. For the most part, these are non-alphanumeric\n * or function keys, some which may require special treatment (eg, preventDefault() if returning false on\n * the initial keyDown event is insufficient).\n *\n * keyCodes for most common ASCII keys can simply use the appropriate ASCII code above.\n *\n * Most of these represent non-ASCII keys (eg, the LEFT arrow key), yet for some reason, browsers defined\n * them using ASCII codes (eg, the LEFT arrow key uses the ASCII code for '%' or 37).\n */\n KEYCODE: {\n /* 0x08 */ BS: 8, // BACKSPACE (ASCII.CTRL_H)\n /* 0x09 */ TAB: 9, // TAB (ASCII.CTRL_I)\n /* 0x0A */ LF: 10, // LINE-FEED (ASCII.CTRL_J) (Some Windows-based browsers used to generate this via CTRL-ENTER)\n /* 0x0D */ CR: 13, // CARRIAGE RETURN (ASCII.CTRL_M)\n /* 0x10 */ SHIFT: 16,\n /* 0x11 */ CTRL: 17,\n /* 0x12 */ ALT: 18,\n /* 0x13 */ PAUSE: 19, // PAUSE/BREAK\n /* 0x14 */ CAPS_LOCK: 20,\n /* 0x1B */ ESC: 27,\n /* 0x20 */ SPACE: 32,\n /* 0x21 */ PGUP: 33,\n /* 0x22 */ PGDN: 34,\n /* 0x23 */ END: 35,\n /* 0x24 */ HOME: 36,\n /* 0x25 */ LEFT: 37,\n /* 0x26 */ UP: 38,\n /* 0x27 */ RIGHT: 39,\n /* 0x27 */ FF_QUOTE: 39,\n /* 0x28 */ DOWN: 40,\n /* 0x2C */ FF_COMMA: 44,\n /* 0x2C */ PRTSC: 44,\n /* 0x2D */ INS: 45,\n /* 0x2E */ DEL: 46,\n /* 0x2E */ FF_PERIOD: 46,\n /* 0x2F */ FF_SLASH: 47,\n /* 0x30 */ ZERO: 48,\n /* 0x31 */ ONE: 49,\n /* 0x32 */ TWO: 50,\n /* 0x33 */ THREE: 51,\n /* 0x34 */ FOUR: 52,\n /* 0x35 */ FIVE: 53,\n /* 0x36 */ SIX: 54,\n /* 0x37 */ SEVEN: 55,\n /* 0x38 */ EIGHT: 56,\n /* 0x39 */ NINE: 57,\n /* 0x3B */ FF_SEMI: 59,\n /* 0x3D */ FF_EQUALS: 61,\n /* 0x5B */ CMD: 91, // aka WIN\n /* 0x5B */ FF_LBRACK: 91,\n /* 0x5C */ FF_BSLASH: 92,\n /* 0x5D */ RCMD: 93, // aka MENU\n /* 0x5D */ FF_RBRACK: 93,\n /* 0x60 */ NUM_0: 96,\n /* 0x60 */ NUM_INS: 96,\n /* 0x60 */ FF_BQUOTE: 96,\n /* 0x61 */ NUM_1: 97,\n /* 0x61 */ NUM_END: 97,\n /* 0x62 */ NUM_2: 98,\n /* 0x62 */ NUM_DOWN: 98,\n /* 0x63 */ NUM_3: 99,\n /* 0x63 */ NUM_PGDN: 99,\n /* 0x64 */ NUM_4: 100,\n /* 0x64 */ NUM_LEFT: 100,\n /* 0x65 */ NUM_5: 101,\n /* 0x65 */ NUM_CENTER: 101,\n /* 0x66 */ NUM_6: 102,\n /* 0x66 */ NUM_RIGHT: 102,\n /* 0x67 */ NUM_7: 103,\n /* 0x67 */ NUM_HOME: 103,\n /* 0x68 */ NUM_8: 104,\n /* 0x68 */ NUM_UP: 104,\n /* 0x69 */ NUM_9: 105,\n /* 0x69 */ NUM_PGUP: 105,\n /* 0x6A */ NUM_MUL: 106,\n /* 0x6B */ NUM_ADD: 107,\n /* 0x6D */ NUM_SUB: 109,\n /* 0x6E */ NUM_DEL: 110, // aka PERIOD\n /* 0x6F */ NUM_DIV: 111,\n /* 0x70 */ F1: 112,\n /* 0x71 */ F2: 113,\n /* 0x72 */ F3: 114,\n /* 0x73 */ F4: 115,\n /* 0x74 */ F5: 116,\n /* 0x75 */ F6: 117,\n /* 0x76 */ F7: 118,\n /* 0x77 */ F8: 119,\n /* 0x78 */ F9: 120,\n /* 0x79 */ F10: 121,\n /* 0x7A */ F11: 122,\n /* 0x7B */ F12: 123,\n /* 0x90 */ NUM_LOCK: 144,\n /* 0x91 */ SCROLL_LOCK: 145,\n /* 0xAD */ FF_DASH: 173,\n /* 0xBA */ SEMI: 186, // Firefox: 59 (FF_SEMI)\n /* 0xBB */ EQUALS: 187, // Firefox: 61 (FF_EQUALS)\n /* 0xBC */ COMMA: 188,\n /* 0xBD */ DASH: 189, // Firefox: 173 (FF_DASH)\n /* 0xBE */ PERIOD: 190,\n /* 0xBF */ SLASH: 191,\n /* 0xC0 */ BQUOTE: 192,\n /* 0xDB */ LBRACK: 219,\n /* 0xDC */ BSLASH: 220,\n /* 0xDD */ RBRACK: 221,\n /* 0xDE */ QUOTE: 222,\n /* 0xE0 */ FF_CMD: 224, // Firefox only (used for both CMD and RCMD)\n //\n // The following biases use what I'll call Decimal Coded Binary or DCB (the opposite of BCD),\n // where the thousands digit is used to store the sum of \"binary\" digits 1 and/or 2 and/or 4.\n //\n // Technically, that makes it DCO (Decimal Coded Octal), but then again, BCD should have really\n // been called HCD (Hexadecimal Coded Decimal), so if \"they\" can take liberties, so can I.\n //\n // ONDOWN is a bias we add to browser keyCodes that we want to handle on \"down\" rather than on \"press\".\n //\n ONDOWN: 1000,\n //\n // ONRIGHT is a bias we add to browser keyCodes that need to check for a \"right\" location (default is \"left\")\n //\n ONRIGHT: 2000,\n //\n // FAKE is a bias we add to signal these are fake keyCodes corresponding to internal keystroke combinations.\n // The actual values are for internal use only and merely need to be unique and used consistently.\n //\n FAKE: 4000\n },\n /*\n * The set of values that a browser may store in the 'location' property of a keyboard event object\n * which we also support.\n */\n LOCATION: {\n LEFT: 1,\n RIGHT: 2,\n NUMPAD: 3\n }\n};\n\n/*\n * Check the event object's 'location' property for a non-zero value for the following ONRIGHT keys.\n */\nKeys.KEYCODE.NUM_CR = Keys.KEYCODE.CR + Keys.KEYCODE.ONRIGHT;\n\n\n/*\n * Maps Firefox keyCodes to their more common keyCode counterparts; a number of entries in this table\n * are no longer valid (if indeed they ever were), so they've been commented out. It's likely that I\n * simply extended this table to resolve additional differences in other browsers (ie, Opera), but without\n * browser-specific checks, it's not safe to perform all the mappings shown below.\n */\nKeys.FF_KEYCODES = {};\nKeys.FF_KEYCODES[Keys.KEYCODE.FF_SEMI] = Keys.KEYCODE.SEMI; // 59 -> 186\nKeys.FF_KEYCODES[Keys.KEYCODE.FF_EQUALS] = Keys.KEYCODE.EQUALS; // 61 -> 187\nKeys.FF_KEYCODES[Keys.KEYCODE.FF_DASH] = Keys.KEYCODE.DASH; // 173 -> 189\nKeys.FF_KEYCODES[Keys.KEYCODE.FF_CMD] = Keys.KEYCODE.CMD; // 224 -> 91\n// Keys.FF_KEYCODES[Keys.KEYCODE.FF_COMMA] = Keys.KEYCODE.COMMA; // 44 -> 188\n// Keys.FF_KEYCODES[Keys.KEYCODE.FF_PERIOD] = Keys.KEYCODE.PERIOD; // 46 -> 190\n// Keys.FF_KEYCODES[Keys.KEYCODE.FF_SLASH] = Keys.KEYCODE.SLASH; // 47 -> 191\n// Keys.FF_KEYCODES[Keys.KEYCODE.FF_BQUOTE] = Keys.KEYCODE.BQUOTE; // 96 -> 192\n// Keys.FF_KEYCODES[Keys.KEYCODE.FF_LBRACK = Keys.KEYCODE.LBRACK; // 91 -> 219\n// Keys.FF_KEYCODES[Keys.KEYCODE.FF_BSLASH] = Keys.KEYCODE.BSLASH; // 92 -> 220\n// Keys.FF_KEYCODES[Keys.KEYCODE.FF_RBRACK] = Keys.KEYCODE.RBRACK; // 93 -> 221\n// Keys.FF_KEYCODES[Keys.KEYCODE.FF_QUOTE] = Keys.KEYCODE.QUOTE; // 39 -> 222\n\n/*\n * Maps non-ASCII keyCodes to their ASCII counterparts\n */\nKeys.NONASCII_KEYCODES = {};\nKeys.NONASCII_KEYCODES[Keys.KEYCODE.FF_DASH] = Keys.ASCII['-']; // 173 -> 45\nKeys.NONASCII_KEYCODES[Keys.KEYCODE.SEMI] = Keys.ASCII[';']; // 186 -> 59\nKeys.NONASCII_KEYCODES[Keys.KEYCODE.EQUALS] = Keys.ASCII['=']; // 187 -> 61\nKeys.NONASCII_KEYCODES[Keys.KEYCODE.DASH] = Keys.ASCII['-']; // 189 -> 45\nKeys.NONASCII_KEYCODES[Keys.KEYCODE.COMMA] = Keys.ASCII[',']; // 188 -> 44\nKeys.NONASCII_KEYCODES[Keys.KEYCODE.PERIOD] = Keys.ASCII['.']; // 190 -> 46\nKeys.NONASCII_KEYCODES[Keys.KEYCODE.SLASH] = Keys.ASCII['/']; // 191 -> 47\nKeys.NONASCII_KEYCODES[Keys.KEYCODE.BQUOTE] = Keys.ASCII['`']; // 192 -> 96\nKeys.NONASCII_KEYCODES[Keys.KEYCODE.LBRACK] = Keys.ASCII['[']; // 219 -> 91\nKeys.NONASCII_KEYCODES[Keys.KEYCODE.BSLASH] = Keys.ASCII['\\\\']; // 220 -> 92\nKeys.NONASCII_KEYCODES[Keys.KEYCODE.RBRACK] = Keys.ASCII[']']; // 221 -> 93\nKeys.NONASCII_KEYCODES[Keys.KEYCODE.QUOTE] = Keys.ASCII[\"'\"]; // 222 -> 39\n\n/*\n * Maps unshifted keyCodes to their shifted counterparts; to be used when a shift-key is down.\n * Alphabetic characters are handled in code, since they must also take CAPS_LOCK into consideration.\n */\nKeys.SHIFTED_KEYCODES = {};\nKeys.SHIFTED_KEYCODES[Keys.ASCII['1']] = Keys.ASCII['!'];\nKeys.SHIFTED_KEYCODES[Keys.ASCII['2']] = Keys.ASCII['@'];\nKeys.SHIFTED_KEYCODES[Keys.ASCII['3']] = Keys.ASCII['#'];\nKeys.SHIFTED_KEYCODES[Keys.ASCII['4']] = Keys.ASCII['$'];\nKeys.SHIFTED_KEYCODES[Keys.ASCII['5']] = Keys.ASCII['%'];\nKeys.SHIFTED_KEYCODES[Keys.ASCII['6']] = Keys.ASCII['^'];\nKeys.SHIFTED_KEYCODES[Keys.ASCII['7']] = Keys.ASCII['&'];\nKeys.SHIFTED_KEYCODES[Keys.ASCII['8']] = Keys.ASCII['*'];\nKeys.SHIFTED_KEYCODES[Keys.ASCII['9']] = Keys.ASCII['('];\nKeys.SHIFTED_KEYCODES[Keys.ASCII['0']] = Keys.ASCII[')'];\nKeys.SHIFTED_KEYCODES[Keys.KEYCODE.SEMI] = Keys.ASCII[':'];\nKeys.SHIFTED_KEYCODES[Keys.KEYCODE.EQUALS] = Keys.ASCII['+'];\nKeys.SHIFTED_KEYCODES[Keys.KEYCODE.COMMA] = Keys.ASCII['<'];\nKeys.SHIFTED_KEYCODES[Keys.KEYCODE.DASH] = Keys.ASCII['_'];\nKeys.SHIFTED_KEYCODES[Keys.KEYCODE.PERIOD] = Keys.ASCII['>'];\nKeys.SHIFTED_KEYCODES[Keys.KEYCODE.SLASH] = Keys.ASCII['?'];\nKeys.SHIFTED_KEYCODES[Keys.KEYCODE.BQUOTE] = Keys.ASCII['~'];\nKeys.SHIFTED_KEYCODES[Keys.KEYCODE.LBRACK] = Keys.ASCII['{'];\nKeys.SHIFTED_KEYCODES[Keys.KEYCODE.BSLASH] = Keys.ASCII['|'];\nKeys.SHIFTED_KEYCODES[Keys.KEYCODE.RBRACK] = Keys.ASCII['}'];\nKeys.SHIFTED_KEYCODES[Keys.KEYCODE.QUOTE] = Keys.ASCII['\"'];\nKeys.SHIFTED_KEYCODES[Keys.KEYCODE.FF_DASH] = Keys.ASCII['_'];\nKeys.SHIFTED_KEYCODES[Keys.KEYCODE.FF_EQUALS] = Keys.ASCII['+'];\nKeys.SHIFTED_KEYCODES[Keys.KEYCODE.FF_SEMI] = Keys.ASCII[':'];\n\n\n\n/**\n * @copyright https://www.pcjs.org/modules/shared/lib/strlib.js (C) Jeff Parsons 2012-2019\n */\n\nclass Str {\n /**\n * isValidInt(s, base)\n *\n * The built-in parseInt() function has the annoying feature of returning a partial value (ie,\n * up to the point where it encounters an invalid character); eg, parseInt(\"foo\", 16) returns 0xf.\n *\n * So it's best to use our own Str.parseInt() function, which will in turn use this function to\n * validate the entire string.\n *\n * @param {string} s is the string representation of some number\n * @param {number} [base] is the radix to use (default is 10); only 2, 8, 10 and 16 are supported\n * @return {boolean} true if valid, false if invalid (or the specified base isn't supported)\n */\n static isValidInt(s, base)\n {\n if (!base || base == 10) return s.match(/^-?[0-9]+$/) !== null;\n if (base == 16) return s.match(/^-?[0-9a-f]+$/i) !== null;\n if (base == 8) return s.match(/^-?[0-7]+$/) !== null;\n if (base == 2) return s.match(/^-?[01]+$/) !== null;\n return false;\n }\n\n /**\n * parseInt(s, base)\n *\n * This is a wrapper around the built-in parseInt() function. Our wrapper recognizes certain prefixes\n * ('$' or \"0x\" for hex, '#' or \"0o\" for octal) and suffixes ('.' for decimal, 'h' for hex, 'y' for\n * binary), and then calls isValidInt() to ensure we don't convert strings that contain partial values;\n * see isValidInt() for details.\n *\n * The use of multiple prefix/suffix combinations is undefined (although for the record, we process\n * prefixes first). We do NOT support the \"0b\" prefix to indicate binary UNLESS one or more commas are\n * also present (because \"0b\" is also a valid hex sequence), and we do NOT support a single leading zero\n * to indicate octal (because such a number could also be decimal or hex). Any number of commas are\n * allowed; we remove them all before calling the built-in parseInt().\n *\n * More recently, we've added support for \"^D\", \"^O\", and \"^B\" prefixes to accommodate the base overrides\n * that the PDP-10's MACRO-10 assembly language supports (decimal, octal, and binary, respectively).\n * If this support turns out to adversely affect other debuggers, then it will have to be \"conditionalized\".\n * Similarly, we've added support for \"K\", \"M\", and \"G\" MACRO-10-style suffixes that add 3, 6, or 9 zeros\n * to the value to be parsed, respectively.\n *\n * @param {string} s is the string representation of some number\n * @param {number} [base] is the radix to use (default is 10); can be overridden by prefixes/suffixes\n * @return {number|undefined} corresponding value, or undefined if invalid\n */\n static parseInt(s, base)\n {\n let value;\n\n if (s) {\n if (!base) base = 10;\n\n let ch, chPrefix, chSuffix;\n let fCommas = (s.indexOf(',') > 0);\n if (fCommas) s = s.replace(/,/g, '');\n\n ch = chPrefix = s.charAt(0);\n if (chPrefix == '#') {\n base = 8;\n chPrefix = '';\n }\n else if (chPrefix == '$') {\n base = 16;\n chPrefix = '';\n }\n if (ch != chPrefix) {\n s = s.substr(1);\n }\n else {\n ch = chPrefix = s.substr(0, 2);\n if (chPrefix == '0b' && fCommas || chPrefix == '^B') {\n base = 2;\n chPrefix = '';\n }\n else if (chPrefix == '0o' || chPrefix == '^O') {\n base = 8;\n chPrefix = '';\n }\n else if (chPrefix == '^D') {\n base = 10;\n chPrefix = '';\n }\n else if (chPrefix == '0x') {\n base = 16;\n chPrefix = '';\n }\n if (ch != chPrefix) s = s.substr(2);\n }\n ch = chSuffix = s.slice(-1);\n if (chSuffix == 'Y' || chSuffix == 'y') {\n base = 2;\n chSuffix = '';\n }\n else if (chSuffix == '.') {\n base = 10;\n chSuffix = '';\n }\n else if (chSuffix == 'H' || chSuffix == 'h') {\n base = 16;\n chSuffix = '';\n }\n else if (chSuffix == 'K') {\n chSuffix = '000';\n }\n else if (chSuffix == 'M') {\n chSuffix = '000000';\n }\n else if (chSuffix == 'G') {\n chSuffix = '000000000';\n }\n if (ch != chSuffix) s = s.slice(0, -1) + chSuffix;\n /*\n * This adds support for the MACRO-10 binary shifting (Bn) suffix, which must be stripped from the\n * number before parsing, and then applied to the value after parsing. If n is omitted, 35 is assumed,\n * which is a net shift of zero. If n < 35, then a left shift of (35 - n) is required; if n > 35, then\n * a right shift of -(35 - n) is required.\n */\n let v, shift = 0;\n if (base <= 10) {\n let match = s.match(/(-?[0-9]+)B([0-9]*)/);\n if (match) {\n s = match[1];\n shift = 35 - ((match[2] || 35) & 0xff);\n }\n }\n if (Str.isValidInt(s, base) && !isNaN(v = parseInt(s, base))) {\n /*\n * With the need to support larger (eg, 36-bit) integers, truncating to 32 bits is no longer helpful.\n *\n * value = v|0;\n */\n if (shift) {\n /*\n * Since binary shifting is a logical operation, and since shifting by division only works properly\n * with positive numbers, we must convert a negative value to a positive value, by computing the two's\n * complement.\n */\n if (v < 0) v += Math.pow(2, 36);\n if (shift > 0) {\n v *= Math.pow(2, shift);\n } else {\n v = Math.trunc(v / Math.pow(2, -shift));\n }\n }\n value = v;\n }\n }\n return value;\n }\n\n /**\n * toBase(n, radix, cch, sPrefix, nGrouping)\n *\n * Displays the given number as an unsigned integer using the specified radix and number of digits.\n *\n * @param {number|*} n\n * @param {number} radix (ie, the base)\n * @param {number} cch (the desired number of digits)\n * @param {string} [sPrefix] (default is none)\n * @param {number} [nGrouping]\n * @return {string}\n */\n static toBase(n, radix, cch, sPrefix = \"\", nGrouping = 0)\n {\n /*\n * We can't rely entirely on isNaN(), because isNaN(null) returns false, and we can't rely\n * entirely on typeof either, because typeof Nan returns \"number\". Sigh.\n *\n * Alternatively, we could mask and shift n regardless of whether it's null/undefined/NaN,\n * since JavaScript coerces such operands to zero, but I think there's \"value\" in seeing those\n * values displayed differently.\n */\n let s = \"\";\n if (isNaN(n) || typeof n != \"number\") {\n n = null;\n } else {\n /*\n * Callers that produced an input by dividing by a power of two rather than shifting (in order\n * to access more than 32 bits) may produce a fractional result, which ordinarily we would simply\n * ignore, but if the integer portion is zero and the sign is negative, we should probably treat\n * this value as a sign-extension.\n */\n if (n < 0 && n > -1) n = -1;\n /*\n * Negative values should be two's complemented according to the number of digits; for example,\n * 12 octal digits implies an upper limit 8^12.\n */\n if (n < 0) {\n n += Math.pow(radix, cch);\n }\n if (n >= Math.pow(radix, cch)) {\n cch = Math.ceil(Math.log(n) / Math.log(radix));\n }\n }\n let g = nGrouping || -1;\n while (cch-- > 0) {\n if (!g) {\n s = ',' + s;\n g = nGrouping;\n }\n if (n == null) {\n s = '?' + s;\n } else {\n let d = n % radix;\n d += (d >= 0 && d <= 9? 0x30 : 0x41 - 10);\n s = String.fromCharCode(d) + s;\n n = Math.trunc(n / radix);\n }\n g--;\n }\n return sPrefix + s;\n }\n\n /**\n * toBin(n, cch, nGrouping)\n *\n * Converts an integer to binary, with the specified number of digits (up to a maximum of 36).\n *\n * @param {number|*} n (supports integers up to 36 bits now)\n * @param {number} [cch] is the desired number of binary digits (0 or undefined for default of either 8, 18, or 36)\n * @param {number} [nGrouping]\n * @return {string} the binary representation of n\n */\n static toBin(n, cch, nGrouping)\n {\n if (!cch) {\n // cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.LN2) || 1;\n let v = Math.abs(n);\n if (v <= 0b11111111) {\n cch = 8;\n } else if (v <= 0b111111111111111111) {\n cch = 18;\n } else {\n cch = 36;\n }\n } else if (cch > 36) cch = 36;\n return Str.toBase(n, 2, cch, \"\", nGrouping);\n }\n\n /**\n * toBinBytes(n, cb, fPrefix)\n *\n * Converts an integer to binary, with the specified number of bytes (up to the default of 4).\n *\n * @param {number|null|undefined} n (interpreted as a 32-bit value)\n * @param {number} [cb] is the desired number of binary bytes (4 is both the default and the maximum)\n * @param {boolean} [fPrefix]\n * @return {string} the binary representation of n\n */\n static toBinBytes(n, cb, fPrefix)\n {\n let s = \"\";\n if (!cb || cb > 4) cb = 4;\n for (let i = 0; i < cb; i++) {\n if (s) s = ',' + s;\n s = Str.toBin(n & 0xff, 8) + s;\n n >>= 8;\n }\n return (fPrefix? \"0b\" : \"\") + s;\n }\n\n /**\n * toOct(n, cch, fPrefix)\n *\n * Converts an integer to octal, with the specified number of digits (default of 6; max of 12)\n *\n * You might be tempted to use the built-in n.toString(8) instead, but it doesn't zero-pad and it\n * doesn't properly convert negative values. Moreover, if n is undefined, n.toString() will throw\n * an exception, whereas this function will return '?' characters.\n *\n * @param {number|*} n (supports integers up to 36 bits now)\n * @param {number} [cch] is the desired number of octal digits (0 or undefined for default of either 6, 8, or 12)\n * @param {boolean} [fPrefix]\n * @return {string} the octal representation of n\n */\n static toOct(n, cch, fPrefix)\n {\n if (!cch) {\n // cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.log(8)) || 1;\n let v = Math.abs(n);\n if (v <= 0o777777) {\n cch = 6;\n } else if (v <= 0o77777777) {\n cch = 8;\n } else {\n cch = 12;\n }\n } else if (cch > 12) cch = 12;\n return Str.toBase(n, 8, cch, fPrefix? \"0o\" : \"\");\n }\n\n /**\n * toDec(n, cch)\n *\n * Converts an integer to decimal, with the specified number of digits (default of 5; max of 11)\n *\n * You might be tempted to use the built-in n.toString(10) instead, but it doesn't zero-pad and it\n * doesn't properly convert negative values. Moreover, if n is undefined, n.toString() will throw\n * an exception, whereas this function will return '?' characters.\n *\n * @param {number|*} n (supports integers up to 36 bits now)\n * @param {number} [cch] is the desired number of decimal digits (0 or undefined for default of either 5 or 11)\n * @return {string} the decimal representation of n\n */\n static toDec(n, cch)\n {\n if (!cch) {\n // cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.LN10) || 1;\n let v = Math.abs(n);\n if (v <= 99999) {\n cch = 5;\n } else {\n cch = 11;\n }\n } else if (cch > 11) cch = 11;\n return Str.toBase(n, 10, cch);\n }\n\n /**\n * toHex(n, cch, fPrefix)\n *\n * Converts an integer to hex, with the specified number of digits (default of 4 or 8, max of 9).\n *\n * You might be tempted to use the built-in n.toString(16) instead, but it doesn't zero-pad and it\n * doesn't properly convert negative values; for example, if n is -2147483647, then n.toString(16)\n * will return \"-7fffffff\" instead of \"80000001\". Moreover, if n is undefined, n.toString() will\n * throw an exception, whereas this function will return '?' characters.\n *\n * NOTE: The following work-around (adapted from code found on StackOverflow) would be another solution,\n * taking care of negative values, zero-padding, and upper-casing, but not null/undefined/NaN values:\n *\n * s = (n < 0? n + 0x100000000 : n).toString(16);\n * s = \"00000000\".substr(0, 8 - s.length) + s;\n * s = s.substr(0, cch).toUpperCase();\n *\n * @param {number|*} n (supports integers up to 36 bits now)\n * @param {number} [cch] is the desired number of hex digits (0 or undefined for default of either 4, 8, or 9)\n * @param {boolean} [fPrefix]\n * @return {string} the hex representation of n\n */\n static toHex(n, cch, fPrefix)\n {\n if (!cch) {\n // cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.log(16)) || 1;\n let v = Math.abs(n);\n if (v <= 0xffff) {\n cch = 4;\n } else if (v <= 0xffffffff) {\n cch = 8;\n } else {\n cch = 9;\n }\n } else if (cch > 9) cch = 9;\n return Str.toBase(n, 16, cch, fPrefix? \"0x\" : \"\");\n }\n\n /**\n * toHexByte(b)\n *\n * Alias for Str.toHex(b, 2, true)\n *\n * @param {number|null|undefined} b is a byte value\n * @return {string} the hex representation of b\n */\n static toHexByte(b)\n {\n return Str.toHex(b, 2, true);\n }\n\n /**\n * toHexWord(w)\n *\n * Alias for Str.toHex(w, 4, true)\n *\n * @param {number|null|undefined} w is a word (16-bit) value\n * @return {string} the hex representation of w\n */\n static toHexWord(w)\n {\n return Str.toHex(w, 4, true);\n }\n\n /**\n * toHexLong(l)\n *\n * Alias for Str.toHex(l, 8, true)\n *\n * @param {number|null|undefined} l is a dword (32-bit) value\n * @return {string} the hex representation of w\n */\n static toHexLong(l)\n {\n return Str.toHex(l, 8, true);\n }\n\n /**\n * getBaseName(sFileName, fStripExt)\n *\n * This is a poor-man's version of Node's path.basename(), which Node-only components should use instead.\n *\n * Note that if fStripExt is true, this strips ANY extension, whereas path.basename() strips the extension only\n * if it matches the second parameter (eg, path.basename(\"/foo/bar/baz/asdf/quux.html\", \".html\") returns \"quux\").\n *\n * @param {string} sFileName\n * @param {boolean} [fStripExt]\n * @return {string}\n */\n static getBaseName(sFileName, fStripExt)\n {\n let sBaseName = sFileName;\n\n let i = sFileName.lastIndexOf('/');\n if (i >= 0) sBaseName = sFileName.substr(i + 1);\n\n /*\n * This next bit is a kludge to clean up names that are part of a URL that includes unsightly query parameters.\n */\n i = sBaseName.indexOf('&');\n if (i > 0) sBaseName = sBaseName.substr(0, i);\n\n if (fStripExt) {\n i = sBaseName.lastIndexOf(\".\");\n if (i > 0) {\n sBaseName = sBaseName.substring(0, i);\n }\n }\n return sBaseName;\n }\n\n /**\n * getExtension(sFileName)\n *\n * This is a poor-man's version of Node's path.extname(), which Node-only components should use instead.\n *\n * Note that we EXCLUDE the period from the returned extension, whereas path.extname() includes it.\n *\n * @param {string} sFileName\n * @return {string} the filename's extension (in lower-case and EXCLUDING the \".\"), or an empty string\n */\n static getExtension(sFileName)\n {\n let sExtension = \"\";\n let i = sFileName.lastIndexOf(\".\");\n if (i >= 0) {\n sExtension = sFileName.substr(i + 1).toLowerCase();\n }\n return sExtension;\n }\n\n /**\n * endsWith(s, sSuffix)\n *\n * @param {string} s\n * @param {string} sSuffix\n * @return {boolean} true if s ends with sSuffix, false if not\n */\n static endsWith(s, sSuffix)\n {\n return s.indexOf(sSuffix, s.length - sSuffix.length) !== -1;\n }\n\n /**\n * escapeHTML(sHTML)\n *\n * @param {string} sHTML\n * @return {string} with special characters \"escaped\" as HTML entities, similar to PHP's htmlspecialchars()\n */\n static escapeHTML(sHTML)\n {\n /*\n * Most recently, '$' was added to the list to help avoid problems when callers use the resulting string\n * as a replacement string for JavaScript's string replace() function, which treats '$' specially. Technically,\n * that's on the callers of replace(), not us, but this doesn't seem harmful, and it's definitely helpful.\n */\n return sHTML.replace(/[&<>\"'$]/g, function(m)\n {\n return Str.HTMLEscapeMap[m];\n });\n }\n\n /**\n * replace(sSearch, sReplace, s)\n *\n * The JavaScript replace() function ALWAYS interprets \"$\" specially in replacement strings, even when\n * the search string is NOT a RegExp; specifically:\n *\n * $$ Inserts a \"$\"\n * $& Inserts the matched substring\n * $` Inserts the portion of the string that precedes the matched substring\n * $' Inserts the portion of the string that follows the matched substring\n * $n Where n is a positive integer less than 100, inserts the nth parenthesized sub-match string,\n * provided the first argument was a RegExp object\n *\n * So, if a replacement string containing dollar signs passes through a series of replace() calls, untold\n * problems could result. Hence, this function, which simply uses the replacement string as-is.\n *\n * Similar to the JavaScript replace() method (when sSearch is a string), this replaces only ONE occurrence\n * (ie, the FIRST occurrence); it might be nice to add options to replace the LAST occurrence and/or ALL\n * occurrences, but we'll revisit that later.\n *\n * @param {string} sSearch\n * @param {string} sReplace\n * @param {string} s\n * @return {string}\n */\n static replace(sSearch, sReplace, s)\n {\n let i = s.indexOf(sSearch);\n if (i >= 0) {\n s = s.substr(0, i) + sReplace + s.substr(i + sSearch.length);\n }\n return s;\n }\n\n /**\n * replaceAll(sSearch, sReplace, s)\n *\n * @param {string} sSearch\n * @param {string} sReplace\n * @param {string} s\n * @return {string}\n */\n static replaceAll(sSearch, sReplace, s)\n {\n let a = {};\n a[sSearch] = sReplace;\n return Str.replaceArray(a, s);\n }\n\n /**\n * replaceArray(a, s)\n *\n * @param {Object} a\n * @param {string} s\n * @return {string}\n */\n static replaceArray(a, s)\n {\n let sMatch = \"\";\n for (let k in a) {\n /*\n * As noted in:\n *\n * http://www.regexguru.com/2008/04/escape-characters-only-when-necessary/\n *\n * inside character classes, only backslash, caret, hyphen and the closing bracket need to be\n * escaped. And in fact, if you ensure that the closing bracket is first, the caret is not first,\n * and the hyphen is last, you can avoid escaping those as well.\n */\n k = k.replace(/([\\\\[\\]*{}().+?|$])/g, \"\\\\$1\");\n sMatch += (sMatch? '|' : '') + k;\n }\n return s.replace(new RegExp('(' + sMatch + ')', \"g\"), function(m)\n {\n return a[m];\n });\n }\n\n /**\n * pad(s, cch, fPadLeft)\n *\n * NOTE: the maximum amount of padding currently supported is 40 spaces.\n *\n * @param {string} s is a string\n * @param {number} cch is desired length\n * @param {boolean} [fPadLeft] (default is padding on the right)\n * @return {string} the original string (s) with spaces padding it to the specified length\n */\n static pad(s, cch, fPadLeft)\n {\n let sPadding = \" \";\n return fPadLeft? (sPadding + s).slice(-cch) : (s + sPadding).slice(0, cch);\n }\n\n /**\n * parseDate(date)\n * parseDate(date, time)\n * parseDate(year, month, day, hour, minute, second)\n *\n * Produces a UTC date when ONLY a date (no time) is provided; otherwise, it combines the date and\n * and time, producing a date that is either UTC or local, depending on the presence (or lack) of time\n * zone information. Finally, if numeric inputs are provided, then Date.UTC() is called to generate\n * a UTC time.\n *\n * In general, you should use this instead of new Date(s), because the Date constructor implicitly calls\n * Date.parse(s), which behaves inconsistently. For example, ISO date-only strings (e.g. \"1970-01-01\")\n * generate a UTC time, but non-ISO date-only strings (eg, \"10/1/1945\" or \"October 1, 1945\") generate a\n * local time.\n *\n * @param {...} args\n * @return {Date} (UTC unless a time string with a non-GMT timezone is explicitly provided)\n */\n static parseDate(...args)\n {\n let date;\n if (args[0] === undefined) {\n date = new Date(Date.now());\n }\n else if (typeof args[0] === \"string\") {\n date = new Date(args[0] + ' ' + (args[1] || \"00:00:00 GMT\"));\n }\n else if (args[1] === undefined) {\n date = new Date(args[0]);\n } else {\n date = new Date(Date.UTC(...args));\n }\n return date;\n }\n\n /**\n * isValidDate(date)\n *\n * @param {Date} date\n * @return {boolean}\n */\n static isValidDate(date)\n {\n return !isNaN(date.getTime());\n }\n\n /**\n * sprintf(format, ...args)\n *\n * Copied from the CCjs project (https://github.com/jeffpar/ccjs/blob/master/lib/stdio.js) and extended.\n *\n * Far from complete, let alone sprintf-compatible, but it's adequate for the handful of sprintf-style format\n * specifiers that I use.\n *\n * TODO: The %c and %s specifiers support a negative width (for left-justified output), but the numeric specifiers\n * (eg, %d and %x) do not; they support only positive widths and right-justified output. That's one of the more\n * glaring omissions at the moment.\n *\n * @param {string} format\n * @param {...} args\n * @return {string}\n */\n static sprintf(format, ...args)\n {\n let text = \"\";\n let aParts = format.split(/%([-+ 0#]*)([0-9]*|\\*)(\\.[0-9]+|)([hlL]?)([A-Za-z%])/);\n\n let iArg = 0, iPart;\n for (iPart = 0; iPart < aParts.length - 6; iPart += 6) {\n\n text += aParts[iPart];\n let arg, type = aParts[iPart+5];\n\n /*\n * Check for unrecognized types immediately, so we don't inadvertently pop any arguments;\n * the first 12 (\"ACDFHIMNSTWY\") are for our non-standard Date extensions (see below).\n *\n * For reference purposes, the standard ANSI C set of format types is: \"dioxXucsfeEgGpn%\".\n */\n let iType = \"ACDFHIMNSTWYdfjcsoXx%\".indexOf(type);\n if (iType < 0) {\n text += '%' + aParts[iPart+1] + aParts[iPart+2] + aParts[iPart+3] + aParts[iPart+4] + type;\n continue;\n }\n\n if (iArg < args.length) {\n arg = args[iArg];\n if (type != '%') iArg++;\n } else {\n arg = args[args.length-1];\n }\n let flags = aParts[iPart+1];\n let width = aParts[iPart+2];\n if (width == '*') {\n width = arg;\n if (iArg < args.length) {\n arg = args[iArg++];\n } else {\n arg = args[args.length-1];\n }\n } else {\n width = +width || 0;\n }\n let precision = aParts[iPart+3];\n precision = precision? +precision.substr(1) : -1;\n // let length = aParts[iPart+4]; // eg, 'h', 'l' or 'L' (all currently ignored)\n let hash = flags.indexOf('#') >= 0;\n let ach = null, s, radix = 0, p, prefix = \"\";\n\n /*\n * The following non-standard sprintf() format codes provide handy alternatives to the\n * PHP date() format codes that we used to use with the old datelib.formatDate() function:\n *\n * a: lowercase ante meridiem and post meridiem (am or pm) %A\n * d: day of the month, 2 digits with leading zeros (01, 02, ..., 31) %02D\n * D: 3-letter day of the week (\"Sun\", \"Mon\", ..., \"Sat\") %.3W\n * F: month (\"January\", \"February\", ..., \"December\") %F\n * g: hour in 12-hour format, without leading zeros (1, 2, ..., 12) %I\n * h: hour in 24-hour format, without leading zeros (0, 1, ..., 23) %H\n * H: hour in 24-hour format, with leading zeros (00, 01, ..., 23) %02H\n * i: minutes, with leading zeros (00, 01, ..., 59) %02N\n * j: day of the month, without leading zeros (1, 2, ..., 31) %D\n * l: day of the week (\"Sunday\", \"Monday\", ..., \"Saturday\") %W\n * m: month, with leading zeros (01, 02, ..., 12) %02M\n * M: 3-letter month (\"Jan\", \"Feb\", ..., \"Dec\") %.3F\n * n: month, without leading zeros (1, 2, ..., 12) %M\n * s: seconds, with leading zeros (00, 01, ..., 59) %02S\n * y: 2-digit year (eg, 14) %0.2Y\n * Y: 4-digit year (eg, 2014) %Y\n *\n * We also support a few custom format codes:\n *\n * %C: calendar output (equivalent to: %W, %F %D, %Y)\n * %T: timestamp output (equivalent to: %Y-%02M-%02D %02H:%02N:%02S)\n *\n * Use the optional '#' flag with any of the above '%' format codes to produce UTC results\n * (eg, '%#I' instead of '%I').\n *\n * The %A, %F, and %W types act as strings (which support the '-' left justification flag, as well as\n * the width and precision options), and the rest act as integers (which support the '0' padding flag\n * and the width option). Also, while %Y does act as an integer, it also supports truncation using the\n * precision option (normally, integers do not); this enables a variable number of digits for the year.\n *\n * So old code like this:\n *\n * printf(\"%s\\n\", formatDate(\"l, F j, Y\", date));\n *\n * can now be written like this:\n *\n * printf(\"%W, %F %D, %Y\\n\", date, date, date, date);\n *\n * or even more succinctly, as:\n *\n * printf(\"%C\\n\", date);\n */\n let ch, date = /** @type {Date} */ (iType < 12 && typeof arg != \"object\"? Str.parseDate(arg) : arg), dateUndefined;\n\n switch(type) {\n case 'C':\n ch = hash? '#' : '';\n text += (Str.isValidDate(date)? Str.sprintf(Str.sprintf(\"%%%sW, %%%sF %%%sD, %%%sY\", ch), date) : dateUndefined);\n continue;\n\n case 'D':\n arg = hash? date.getUTCDate() : date.getDate();\n type = 'd';\n break;\n\n case 'A':\n case 'H':\n case 'I':\n arg = hash? date.getUTCHours() : date.getHours();\n if (type == 'A') {\n arg = (arg < 12 ? \"am\" : \"pm\");\n type = 's';\n }\n else {\n if (type == 'I') {\n arg = (!arg? 12 : (arg > 12 ? arg - 12 : arg));\n }\n type = 'd';\n }\n break;\n\n case 'F':\n case 'M':\n arg = hash? date.getUTCMonth() : date.getMonth();\n if (type == 'F') {\n arg = Str.NamesOfMonths[arg];\n type = 's';\n } else {\n arg++;\n type = 'd';\n }\n break;\n\n case 'N':\n arg = hash? date.getUTCMinutes() : date.getMinutes();\n type = 'd';\n break;\n\n case 'S':\n arg = hash? date.getUTCSeconds() : date.getSeconds();\n type = 'd'\n break;\n\n case 'T':\n ch = hash? '#' : '';\n text += (Str.isValidDate(date)? Str.sprintf(Str.sprintf(\"%%%sY-%%%s02M-%%%s02D %%%s02H:%%%s02N:%%%s02S\", ch), date) : dateUndefined);\n continue;\n\n case 'W':\n arg = Str.NamesOfDays[hash? date.getUTCDay() : date.getDay()];\n type = 's';\n break;\n\n case 'Y':\n arg = hash? date.getUTCFullYear() : date.getFullYear();\n if (precision > 0) {\n arg = arg % (Math.pow(10, precision));\n precision = -1;\n }\n type = 'd';\n break;\n }\n\n switch(type) {\n case 'd':\n /*\n * We could use \"arg |= 0\", but there may be some value to supporting integers > 32 bits.\n *\n * Also, unlike the 'X' and 'x' hexadecimal cases, there's no need to explicitly check for string\n * arguments, because Math.trunc() automatically coerces any string value to a (decimal) number.\n */\n arg = Math.trunc(arg);\n /* falls through */\n\n case 'f':\n s = arg + \"\";\n if (precision > 0) {\n width -= (precision + 1);\n }\n if (s.length < width) {\n if (flags.indexOf('0') >= 0) {\n if (arg < 0) width--;\n s = (\"0000000000\" + Math.abs(arg)).slice(-width);\n if (arg < 0) s = '-' + s;\n } else {\n s = (\" \" + s).slice(-width);\n }\n }\n if (precision > 0) {\n arg = Math.round((arg - Math.trunc(arg)) * Math.pow(10, precision));\n s += '.' + (\"0000000000\" + Math.abs(arg)).slice(-precision);\n }\n text += s;\n break;\n\n case 'j':\n /*\n * 'j' is one of our non-standard extensions to the sprintf() interface; it signals that\n * the caller is providing an Object that should be rendered as JSON. If a width is included\n * (eg, \"%2j\"), it's used as an indentation value; otherwise, no whitespace is added.\n */\n text += JSON.stringify(arg, null, width || null);\n break;\n\n case 'c':\n arg = typeof arg == \"string\"? arg[0] : String.fromCharCode(arg);\n /* falls through */\n\n case 's':\n /*\n * 's' includes some non-standard behavior, such as coercing non-strings to strings first.\n */\n if (arg !== undefined) {\n if (typeof arg != \"string\") {\n arg = arg.toString();\n }\n if (precision >= 0) {\n arg = arg.substr(0, precision);\n }\n while (arg.length < width) {\n if (flags.indexOf('-') >= 0) {\n arg += ' ';\n } else {\n arg = ' ' + arg;\n }\n }\n }\n text += arg;\n break;\n\n case 'o':\n radix = 8;\n if (hash) prefix = \"0\";\n /* falls through */\n\n case 'X':\n ach = Str.HexUpperCase;\n if (hash) prefix = \"0X\";\n /* falls through */\n\n case 'x':\n s = \"\";\n if (!radix) radix = 16;\n if (!prefix && hash) prefix = \"0x\";\n if (!ach) ach = Str.HexLowerCase;\n if (typeof arg == \"string\") {\n /*\n * Since we're advised to ALWAYS pass a radix to parseInt(), we must detect explicitly\n * hex values ourselves, because using a radix of 10 with any \"0x...\" value always returns 0.\n *\n * And if the value CAN be interpreted as decimal, then we MUST interpret it as decimal, because\n * we have sprintf() calls in /modules/pcx86/lib/testmon.js that depend on this code to perform\n * decimal to hex conversion. We're going to make our own rules here, since passing numbers in\n * string form isn't part of the sprintf \"spec\".\n */\n arg = Number.parseInt(arg, arg.match(/(^0x|[a-f])/i)? 16 : 10);\n }\n p = width? \"\" : prefix;\n do {\n let d = arg & (radix - 1);\n arg >>>= (radix == 16? 4 : 3);\n if (flags.indexOf('0') >= 0 || s == \"\" || d || arg) {\n s = ach[d] + s;\n } else if (width) {\n if (!prefix) {\n s = ' ' + s;\n } else {\n s = prefix.slice(-1) + s;\n prefix = prefix.slice(0, -1);\n }\n }\n } while (--width > 0 || arg);\n text += p + s;\n break;\n\n case '%':\n text += '%';\n break;\n\n default:\n text += \"(unimplemented printf type %\" + type + \")\";\n break;\n }\n }\n\n text += aParts[iPart];\n return text;\n }\n\n /**\n * stripLeadingZeros(s, fPad)\n *\n * @param {string} s\n * @param {boolean} [fPad]\n * @return {string}\n */\n static stripLeadingZeros(s, fPad)\n {\n let cch = s.length;\n s = s.replace(/^0+([0-9A-F]+)$/i, \"$1\");\n if (fPad) s = Str.pad(s, cch, true);\n return s;\n }\n\n /**\n * trim(s)\n *\n * @param {string} s\n * @return {string}\n */\n static trim(s)\n {\n if (String.prototype.trim) {\n return s.trim();\n }\n return s.replace(/^\\s+|\\s+$/g, \"\");\n }\n\n /**\n * toASCIICode(b)\n *\n * @param {number} b\n * @return {string}\n */\n static toASCIICode(b)\n {\n let s;\n if (b != Str.ASCII.CR && b != Str.ASCII.LF) {\n s = Str.ASCIICodeMap[b];\n }\n if (s) {\n s = '<' + s + '>';\n } else {\n s = String.fromCharCode(b);\n }\n return s;\n }\n}\n\n/*\n * Map special characters to their HTML escape sequences.\n */\nStr.HTMLEscapeMap = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n '$': '$'\n};\n\n/*\n * Map \"unprintable\" ASCII codes to mnemonics, to more clearly see what's being printed.\n */\nStr.ASCIICodeMap = {\n 0x00: \"NUL\",\n 0x01: \"SOH\", // (CTRL_A) Start of Heading\n 0x02: \"STX\", // (CTRL_B) Start of Text\n 0x03: \"ETX\", // (CTRL_C) End of Text\n 0x04: \"EOT\", // (CTRL_D) End of Transmission\n 0x05: \"ENQ\", // (CTRL_E) Enquiry\n 0x06: \"ACK\", // (CTRL_F) Acknowledge\n 0x07: \"BEL\", // (CTRL_G) Bell\n 0x08: \"BS\", // (CTRL_H) Backspace\n 0x09: \"TAB\", // (CTRL_I) Horizontal Tab (aka HT)\n 0x0A: \"LF\", // (CTRL_J) Line Feed (New Line)\n 0x0B: \"VT\", // (CTRL_K) Vertical Tab\n 0x0C: \"FF\", // (CTRL_L) Form Feed (New Page)\n 0x0D: \"CR\", // (CTRL_M) Carriage Return\n 0x0E: \"SO\", // (CTRL_N) Shift Out\n 0x0F: \"SI\", // (CTRL_O) Shift In\n 0x10: \"DLE\", // (CTRL_P) Data Link Escape\n 0x11: \"XON\", // (CTRL_Q) Device Control 1 (aka DC1)\n 0x12: \"DC2\", // (CTRL_R) Device Control 2\n 0x13: \"XOFF\", // (CTRL_S) Device Control 3 (aka DC3)\n 0x14: \"DC4\", // (CTRL_T) Device Control 4\n 0x15: \"NAK\", // (CTRL_U) Negative Acknowledge\n 0x16: \"SYN\", // (CTRL_V) Synchronous Idle\n 0x17: \"ETB\", // (CTRL_W) End of Transmission Block\n 0x18: \"CAN\", // (CTRL_X) Cancel\n 0x19: \"EM\", // (CTRL_Y) End of Medium\n 0x1A: \"SUB\", // (CTRL_Z) Substitute\n 0x1B: \"ESC\", // Escape\n 0x1C: \"FS\", // File Separator\n 0x1D: \"GS\", // Group Separator\n 0x1E: \"RS\", // Record Separator\n 0x1F: \"US\", // Unit Separator\n 0x7F: \"DEL\"\n};\n\n/*\n * Refer to: https://en.wikipedia.org/wiki/Code_page_437\n */\nStr.CP437ToUnicode = [\n '\\u0000', '\\u263A', '\\u263B', '\\u2665', '\\u2666', '\\u2663', '\\u2660', '\\u2022',\n '\\u25D8', '\\u25CB', '\\u25D9', '\\u2642', '\\u2640', '\\u266A', '\\u266B', '\\u263C',\n '\\u25BA', '\\u25C4', '\\u2195', '\\u203C', '\\u00B6', '\\u00A7', '\\u25AC', '\\u21A8',\n '\\u2191', '\\u2193', '\\u2192', '\\u2190', '\\u221F', '\\u2194', '\\u25B2', '\\u25BC',\n '\\u0020', '\\u0021', '\\u0022', '\\u0023', '\\u0024', '\\u0025', '\\u0026', '\\u0027',\n '\\u0028', '\\u0029', '\\u002A', '\\u002B', '\\u002C', '\\u002D', '\\u002E', '\\u002F',\n '\\u0030', '\\u0031', '\\u0032', '\\u0033', '\\u0034', '\\u0035', '\\u0036', '\\u0037',\n '\\u0038', '\\u0039', '\\u003A', '\\u003B', '\\u003C', '\\u003D', '\\u003E', '\\u003F',\n '\\u0040', '\\u0041', '\\u0042', '\\u0043', '\\u0044', '\\u0045', '\\u0046', '\\u0047',\n '\\u0048', '\\u0049', '\\u004A', '\\u004B', '\\u004C', '\\u004D', '\\u004E', '\\u004F',\n '\\u0050', '\\u0051', '\\u0052', '\\u0053', '\\u0054', '\\u0055', '\\u0056', '\\u0057',\n '\\u0058', '\\u0059', '\\u005A', '\\u005B', '\\u005C', '\\u005D', '\\u005E', '\\u005F',\n '\\u0060', '\\u0061', '\\u0062', '\\u0063', '\\u0064', '\\u0065', '\\u0066', '\\u0067',\n '\\u0068', '\\u0069', '\\u006A', '\\u006B', '\\u006C', '\\u006D', '\\u006E', '\\u006F',\n '\\u0070', '\\u0071', '\\u0072', '\\u0073', '\\u0074', '\\u0075', '\\u0076', '\\u0077',\n '\\u0078', '\\u0079', '\\u007A', '\\u007B', '\\u007C', '\\u007D', '\\u007E', '\\u2302',\n '\\u00C7', '\\u00FC', '\\u00E9', '\\u00E2', '\\u00E4', '\\u00E0', '\\u00E5', '\\u00E7',\n '\\u00EA', '\\u00EB', '\\u00E8', '\\u00EF', '\\u00EE', '\\u00EC', '\\u00C4', '\\u00C5',\n '\\u00C9', '\\u00E6', '\\u00C6', '\\u00F4', '\\u00F6', '\\u00F2', '\\u00FB', '\\u00F9',\n '\\u00FF', '\\u00D6', '\\u00DC', '\\u00A2', '\\u00A3', '\\u00A5', '\\u20A7', '\\u0192',\n '\\u00E1', '\\u00ED', '\\u00F3', '\\u00FA', '\\u00F1', '\\u00D1', '\\u00AA', '\\u00BA',\n '\\u00BF', '\\u2310', '\\u00AC', '\\u00BD', '\\u00BC', '\\u00A1', '\\u00AB', '\\u00BB',\n '\\u2591', '\\u2592', '\\u2593', '\\u2502', '\\u2524', '\\u2561', '\\u2562', '\\u2556',\n '\\u2555', '\\u2563', '\\u2551', '\\u2557', '\\u255D', '\\u255C', '\\u255B', '\\u2510',\n '\\u2514', '\\u2534', '\\u252C', '\\u251C', '\\u2500', '\\u253C', '\\u255E', '\\u255F',\n '\\u255A', '\\u2554', '\\u2569', '\\u2566', '\\u2560', '\\u2550', '\\u256C', '\\u2567',\n '\\u2568', '\\u2564', '\\u2565', '\\u2559', '\\u2558', '\\u2552', '\\u2553', '\\u256B',\n '\\u256A', '\\u2518', '\\u250C', '\\u2588', '\\u2584', '\\u258C', '\\u2590', '\\u2580',\n '\\u03B1', '\\u00DF', '\\u0393', '\\u03C0', '\\u03A3', '\\u03C3', '\\u00B5', '\\u03C4',\n '\\u03A6', '\\u0398', '\\u03A9', '\\u03B4', '\\u221E', '\\u03C6', '\\u03B5', '\\u2229',\n '\\u2261', '\\u00B1', '\\u2265', '\\u2264', '\\u2320', '\\u2321', '\\u00F7', '\\u2248',\n '\\u00B0', '\\u2219', '\\u00B7', '\\u221A', '\\u207F', '\\u00B2', '\\u25A0', '\\u00A0'\n];\n\n/*\n * TODO: Future home of a complete ASCII table.\n */\nStr.ASCII = {\n LF: 0x0A,\n CR: 0x0D\n};\n\nStr.TYPES = {\n NULL: 0,\n BYTE: 1,\n WORD: 2,\n DWORD: 3,\n NUMBER: 4,\n STRING: 5,\n BOOLEAN: 6,\n OBJECT: 7,\n ARRAY: 8\n};\n\nStr.HexLowerCase = \"0123456789abcdef\";\nStr.HexUpperCase = \"0123456789ABCDEF\";\nStr.NamesOfDays = [\"Sunday\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\"];\nStr.NamesOfMonths = [\"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"];\n\n\n\n/**\n * @copyright https://www.pcjs.org/modules/shared/lib/usrlib.js (C) Jeff Parsons 2012-2019\n */\n\n\n/** @typedef {{ mask: number, shift: number }} */\nvar BitField;\n\n/** @typedef {Object.} */\nvar BitFields;\n\nclass Usr {\n /**\n * binarySearch(a, v, fnCompare)\n *\n * @param {Array} a is an array\n * @param {number|string|Array|Object} v\n * @param {function((number|string|Array|Object), (number|string|Array|Object))} [fnCompare]\n * @return {number} the index of matching entry if non-negative, otherwise the index of the insertion point\n */\n static binarySearch(a, v, fnCompare)\n {\n let left = 0;\n let right = a.length;\n let found = 0;\n if (fnCompare === undefined) {\n fnCompare = function(a, b)\n {\n return a > b ? 1 : a < b ? -1 : 0;\n };\n }\n while (left < right) {\n let middle = (left + right) >> 1;\n let compareResult;\n compareResult = fnCompare(v, a[middle]);\n if (compareResult > 0) {\n left = middle + 1;\n } else {\n right = middle;\n found = !compareResult;\n }\n }\n return found ? left : ~left;\n }\n\n /**\n * binaryInsert(a, v, fnCompare)\n *\n * If element v already exists in array a, the array is unchanged (we don't allow duplicates); otherwise, the\n * element is inserted into the array at the appropriate index.\n *\n * @param {Array} a is an array\n * @param {number|string|Array|Object} v is the value to insert\n * @param {function((number|string|Array|Object), (number|string|Array|Object))} [fnCompare]\n */\n static binaryInsert(a, v, fnCompare)\n {\n let index = Usr.binarySearch(a, v, fnCompare);\n if (index < 0) {\n a.splice(-(index + 1), 0, v);\n }\n }\n\n /**\n * getTimestamp()\n *\n * @return {string} timestamp containing the current date and time (\"yyyy-mm-dd hh:mm:ss\")\n */\n static getTimestamp()\n {\n let date = new Date();\n return Str.sprintf(\"%T\", date);\n }\n\n /**\n * getMonthDays(nMonth, nYear)\n *\n * NOTE: If we're being called on behalf of the PCx86 RTC, its year is always truncated to two digits (mod 100),\n * so we have no idea what century the year 0 might refer to. When using the normal leap-year formula, 0 fails\n * the mod 100 test but passes the mod 400 test, so as far as the RTC is concerned, every century year is a leap\n * year. Since we're most likely dealing with the year 2000, that's fine, since 2000 was also a leap year.\n *\n * TODO: There IS a separate RTC CMOS byte that's supposed to be set to CMOS_ADDR.CENTURY_DATE; it's always BCD,\n * so theoretically it will contain values like 0x19 or 0x20 (for the 20th and 21st centuries, respectively), and\n * we could add that as another parameter to this function, to improve the accuracy, but that would go beyond what\n * a real RTC actually does.\n *\n * @param {number} nMonth (1-12)\n * @param {number} nYear (normally a 4-digit year, but it may also be mod 100)\n * @return {number} the maximum (1-based) day allowed for the specified month and year\n */\n static getMonthDays(nMonth, nYear)\n {\n let nDays = Usr.aMonthDays[nMonth - 1];\n if (nDays == 28) {\n if ((nYear % 4) === 0 && ((nYear % 100) || (nYear % 400) === 0)) {\n nDays++;\n }\n }\n return nDays;\n }\n\n /**\n * adjustDays(date, days)\n *\n * Although the setDate() method compensates for day-of-month values outside the current month:\n *\n * > let d = new Date('11/4/2012');d\n * 2012-11-04T07:00:00.000Z\n * > new Date(d.setDate(d.getDate() + 365))\n * 2014-11-04T08:00:00.000Z\n *\n * notice the discrepancy in the time-of-day. Even if there is some technical reason (eg, a DayLight\n * Savings Time side-effect) why that answer is correct, it doesn't satisfy my goal of adjusting ONLY the\n * day, not the time-of-day.\n *\n * By comparison, the method below (multiplying the number of milliseconds in a day by the number of days)\n * works just fine, without any unexpected side-effects:\n *\n * > let d = new Date('11/4/2012');d\n * 2012-11-04T07:00:00.000Z\n * > new Date(d.getTime() + 365 * 86400000)\n * 2013-11-04T07:00:00.000Z\n *\n * @param {Date} date\n * @param {number} days (+/-)\n * @return {Date}\n */\n static adjustDays(date, days)\n {\n return new Date(date.getTime() + days * 86400000);\n }\n\n /**\n * subtractDays(date1, date2)\n *\n * @param {Date|string} date1\n * @param {Date|string} date2\n * @return {number} (date1 - date2, returned as a signed integer number of days)\n */\n static subtractDays(date1, date2)\n {\n if (typeof date1 == \"string\") date1 = new Date(date1);\n if (typeof date2 == \"string\") date2 = new Date(date2);\n return Math.round((date1.getTime() - date2.getTime()) / 86400000);\n }\n\n /**\n * defineBitFields(bfs)\n *\n * Prepares a bit field definition for use with getBitField() and setBitField(); eg:\n *\n * let bfs = Usr.defineBitFields({num:20, count:8, btmod:1, type:3});\n *\n * 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).\n *\n * Usr.setBitField(bfs.num, n, 1);\n *\n * The above set bit field \"bfs.num\" in numeric variable \"n\" to the value 1.\n *\n * @param {Object} bfs\n * @return {BitFields}\n */\n static defineBitFields(bfs)\n {\n let bit = 0;\n for (let f in bfs) {\n let width = bfs[f];\n let mask = ((1 << width) - 1) << bit;\n bfs[f] = {mask: mask, shift: bit};\n bit += width;\n }\n return bfs;\n }\n\n /**\n * initBitFields(bfs, ...)\n *\n * @param {BitFields} bfs\n * @param {...number} var_args\n * @return {number} a value containing all supplied bit fields\n */\n static initBitFields(bfs, var_args)\n {\n let v = 0, i = 1;\n for (let f in bfs) {\n if (i >= arguments.length) break;\n v = Usr.setBitField(bfs[f], v, arguments[i++]);\n }\n return v;\n }\n\n /**\n * getBitField(bf, v)\n *\n * @param {BitField} bf\n * @param {number} v is a value containing bit fields\n * @return {number} the value of the bit field in v defined by bf\n */\n static getBitField(bf, v)\n {\n return (v & bf.mask) >> bf.shift;\n }\n\n /**\n * setBitField(bf, v, n)\n *\n * @param {BitField} bf\n * @param {number} v is a value containing bit fields\n * @param {number} n is a value to store in v in the bit field defined by bf\n * @return {number} updated v\n */\n static setBitField(bf, v, n)\n {\n return (v & ~bf.mask) | ((n << bf.shift) & bf.mask);\n }\n\n /**\n * indexOf(a, t, i)\n *\n * Use this instead of Array.prototype.indexOf() if you can't be sure the browser supports it.\n *\n * @param {Array} a\n * @param {*} t\n * @param {number} [i]\n * @returns {number}\n */\n static indexOf(a, t, i)\n {\n if (Array.prototype.indexOf) {\n return a.indexOf(t, i);\n }\n i = i || 0;\n if (i < 0) i += a.length;\n if (i < 0) i = 0;\n for (let n = a.length; i < n; i++) {\n if (i in a && a[i] === t) return i;\n }\n return -1;\n }\n}\n\nUsr.aMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n\n/**\n * getTime()\n *\n * @return {number} the current time, in milliseconds\n */\nUsr.getTime = Date.now || function() { return +new Date(); };\n\n\n\n/**\n * @copyright https://www.pcjs.org/modules/shared/lib/weblib.js (C) Jeff Parsons 2012-2019\n */\n\n\n/*\n * According to http://www.w3schools.com/jsref/jsref_obj_global.asp, these are the *global* properties\n * and functions of JavaScript-in-the-Browser:\n *\n * Property Description\n * ---\n * Infinity A numeric value that represents positive/negative infinity\n * NaN \"Not-a-Number\" value\n * undefined Indicates that a variable has not been assigned a value\n *\n * Function Description\n * ---\n * decodeURI() Decodes a URI\n * decodeURIComponent() Decodes a URI component\n * encodeURI() Encodes a URI\n * encodeURIComponent() Encodes a URI component\n * escape() Deprecated in version 1.5. Use encodeURI() or encodeURIComponent() instead\n * eval() Evaluates a string and executes it as if it was script code\n * isFinite() Determines whether a value is a finite, legal number\n * isNaN() Determines whether a value is an illegal number\n * Number() Converts an object's value to a number\n * parseFloat() Parses a string and returns a floating point number\n * parseInt() Parses a string and returns an integer\n * String() Converts an object's value to a string\n * unescape() Deprecated in version 1.5. Use decodeURI() or decodeURIComponent() instead\n *\n * And according to http://www.w3schools.com/jsref/obj_window.asp, these are the properties and functions\n * of the *window* object.\n *\n * Property Description\n * ---\n * closed Returns a Boolean value indicating whether a window has been closed or not\n * defaultStatus Sets or returns the default text in the statusbar of a window\n * document Returns the Document object for the window (See Document object)\n * frames Returns an array of all the frames (including iframes) in the current window\n * history Returns the History object for the window (See History object)\n * innerHeight Returns the inner height of a window's content area\n * innerWidth Returns the inner width of a window's content area\n * length Returns the number of frames (including iframes) in a window\n * location Returns the Location object for the window (See Location object)\n * name Sets or returns the name of a window\n * navigator Returns the Navigator object for the window (See Navigator object)\n * opener Returns a reference to the window that created the window\n * outerHeight Returns the outer height of a window, including toolbars/scrollbars\n * outerWidth Returns the outer width of a window, including toolbars/scrollbars\n * pageXOffset Returns the pixels the current document has been scrolled (horizontally) from the upper left corner of the window\n * pageYOffset Returns the pixels the current document has been scrolled (vertically) from the upper left corner of the window\n * parent Returns the parent window of the current window\n * screen Returns the Screen object for the window (See Screen object)\n * screenLeft Returns the x coordinate of the window relative to the screen\n * screenTop Returns the y coordinate of the window relative to the screen\n * screenX Returns the x coordinate of the window relative to the screen\n * screenY Returns the y coordinate of the window relative to the screen\n * self Returns the current window\n * status Sets or returns the text in the statusbar of a window\n * top Returns the topmost browser window\n *\n * Method Description\n * ---\n * alert() Displays an alert box with a message and an OK button\n * atob() Decodes a base-64 encoded string\n * blur() Removes focus from the current window\n * btoa() Encodes a string in base-64\n * clearInterval() Clears a timer set with setInterval()\n * clearTimeout() Clears a timer set with setTimeout()\n * close() Closes the current window\n * confirm() Displays a dialog box with a message and an OK and a Cancel button\n * createPopup() Creates a pop-up window\n * focus() Sets focus to the current window\n * moveBy() Moves a window relative to its current position\n * moveTo() Moves a window to the specified position\n * open() Opens a new browser window\n * print() Prints the content of the current window\n * prompt() Displays a dialog box that prompts the visitor for input\n * resizeBy() Resizes the window by the specified pixels\n * resizeTo() Resizes the window to the specified width and height\n * scroll() This method has been replaced by the scrollTo() method.\n * scrollBy() Scrolls the content by the specified number of pixels\n * scrollTo() Scrolls the content to the specified coordinates\n * setInterval() Calls a function or evaluates an expression at specified intervals (in milliseconds)\n * setTimeout() Calls a function or evaluates an expression after a specified number of milliseconds\n * stop() Stops the window from loading\n */\n\nclass Web {\n /**\n * log(s, type)\n *\n * For diagnostic output only. DEBUG must be true (or \"--debug\" specified via the command-line)\n * for Component.log() to display anything.\n *\n * @param {string} [s] is the message text\n * @param {string} [type] is the message type\n */\n static log(s, type)\n {\n Component.log(s, type);\n }\n\n /**\n * notice(s, fPrintOnly, id)\n *\n * @param {string} s is the message text\n * @param {boolean} [fPrintOnly]\n * @param {string} [id] is the caller's ID, if any\n */\n static notice(s, fPrintOnly, id)\n {\n Component.notice(s, fPrintOnly, id);\n }\n\n /**\n * alertUser(sMessage)\n *\n * NOTE: Legacy function for older modules (eg, DiskDump); see Component.alertUser().\n *\n * @param {string} sMessage\n */\n static alertUser(sMessage)\n {\n if (window) {\n window.alert(sMessage);\n } else {\n Web.log(sMessage);\n }\n }\n\n /**\n * getResource(sURL, type, fAsync, done, progress)\n *\n * Request the specified resource (sURL), and once the request is complete, notify done().\n *\n * If fAsync is true, a done() callback should ALWAYS be supplied; otherwise, you'll have no\n * idea when the request is complete or what the response was. done() is passed three parameters:\n *\n * done(sURL, resource, nErrorCode)\n *\n * If nErrorCode is zero, resource should contain the requested data; otherwise, an error occurred.\n *\n * If type is set to a string, that string can be used to control the response format;\n * by default, the response format is plain text, but you can specify \"arraybuffer\" to request arbitrary\n * binary data, in which case the returned resource will be a ArrayBuffer rather than a string.\n *\n * @param {string} sURL\n * @param {string|Object|null} [type] (object for POST request, otherwise type of GET request)\n * @param {boolean} [fAsync] is true for an asynchronous request; false otherwise (MUST be set for IE)\n * @param {function(string,string,number)} [done]\n * @param {function(number)} [progress]\n * @return {Array|null} Array containing [resource, nErrorCode], or null if no response available (yet)\n */\n static getResource(sURL, type = \"text\", fAsync = false, done, progress)\n {\n let nErrorCode = 0, resource = null, response = null;\n\n if (typeof resources == 'object' && (resource = resources[sURL])) {\n if (done) done(sURL, resource, nErrorCode);\n return [resource, nErrorCode];\n }\n else if (fAsync && typeof resources == 'function') {\n resources(sURL, function(resource, nErrorCode) {\n if (done) done(sURL, resource, nErrorCode);\n });\n return response;\n }\n\n let sURLRedirect = sURL;\n if (Web.getHost() == \"pcjs:8088\" || NODE) {\n /*\n * The larger resources that I've put on archive.pcjs.org are assumed to also be available locally\n * whenever the hostname is \"pcjs\" (or NODE is true); otherwise, use \"localhost\" when debugging locally.\n *\n * NOTE: http://archive.pcjs.org is currently redirected to https://s3-us-west-2.amazonaws.com/archive.pcjs.org\n */\n sURLRedirect = sURL.replace(/^(http:\\/\\/archive\\.pcjs\\.org\\/|https:\\/\\/[a-z0-9-]+\\.amazonaws\\.com\\/archive\\.pcjs\\.org\\/)([^/]*)\\/(.*?)\\/([^/]*)$/, \"/$2-demo/$3/archive/$4\");\n sURLRedirect = sURLRedirect.replace(/^https:\\/\\/([a-z0-9]+)-disks\\.pcjs\\.org\\/(.*)$/, \"/disks-$1/$2\");\n }\n else {\n /*\n * TODO: Perhaps it's time for our code in netlib.js to finally add support for HTTPS; for now\n * though, it's just as well that the NODE environment assumes all resources are available locally.\n */\n sURLRedirect = sURL.replace(/^\\/disks-([a-z0-9]+)\\//, \"https://$1-disks.pcjs.org/\");\n }\n\n\n let request = (window.XMLHttpRequest? new window.XMLHttpRequest() : new window.ActiveXObject(\"Microsoft.XMLHTTP\"));\n let fArrayBuffer = false, fXHR2 = (typeof request.responseType === 'string');\n\n let callback = function() {\n if (request.readyState !== 4) {\n if (progress) progress(1);\n return null;\n }\n /*\n * The following line was recommended for WebKit, as a work-around to prevent the handler firing multiple\n * times when debugging. Unfortunately, that's not the only XMLHttpRequest problem that occurs when\n * debugging, so I think the WebKit problem is deeper than that. When we have multiple XMLHttpRequests\n * pending, any debugging activity means most of them simply get dropped on floor, so what may actually be\n * happening are mis-notifications rather than redundant notifications.\n *\n * request.onreadystatechange = undefined;\n */\n /*\n * If the request failed due to, say, a CORS policy denial; eg:\n *\n * Failed to load http://www.allbootdisks.com/downloads/Disks/Windows_95_Boot_Disk_Download48/Diskette%20Images/Windows95a.img:\n * Redirect from 'http://www.allbootdisks.com/downloads/Disks/Windows_95_Boot_Disk_Download48/Diskette%20Images/Windows95a.img' to\n * 'http://www.allbootdisks.com/' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.\n * Origin 'http://pcjs:8088' is therefore not allowed access.\n *\n * and our request type was \"arraybuffer\", attempting to access responseText may trigger an exception; eg:\n *\n * Uncaught DOMException: Failed to read the 'responseText' property from 'XMLHttpRequest': The value is only accessible if the object's\n * 'responseType' is '' or 'text' (was 'arraybuffer').\n *\n * We could tiptoe around these potential landmines, but the safest thing to do is wrap this code with try/catch.\n */\n try {\n resource = fArrayBuffer? request.response : request.responseText;\n } catch(err) {\n if (MAXDEBUG) Web.log(\"xmlHTTPRequest(\" + sURLRedirect + \") exception: \" + err.message);\n }\n /*\n * The normal \"success\" case is a non-null resource and an HTTP status code of 200, but when loading files from the\n * local file system (ie, when using the \"file:\" protocol), we have to be a bit more flexible.\n */\n if (resource != null && (request.status == 200 || !request.status && resource.length && Web.getHostProtocol() == \"file:\")) {\n if (MAXDEBUG) Web.log(\"xmlHTTPRequest(\" + sURLRedirect + \"): returned \" + resource.length + \" bytes\");\n }\n else {\n nErrorCode = request.status || -1;\n Web.log(\"xmlHTTPRequest(\" + sURLRedirect + \"): error code \" + nErrorCode);\n if (!request.status && !Web.fAdBlockerWarning) {\n let match = sURLRedirect.match(/(^https?:\\/\\/[^/]+)(.*)/);\n if (match) {\n Web.fAdBlockerWarning = true;\n Component.alertUser(\"PCjs was unable to perform a cross-origin resource request to '\" + match[1] + \"'.\\n\\nIf you're running an ad blocker, try adding '\" + Web.getHostOrigin() + \"' to your whitelist (or find a smarter ad blocker).\");\n }\n }\n }\n if (progress) progress(2);\n if (done) done(sURL, resource, nErrorCode);\n return [resource, nErrorCode];\n };\n\n if (fAsync) {\n request.onreadystatechange = callback;\n }\n\n if (progress) progress(0);\n\n if (type && typeof type == \"object\") {\n let sPost = \"\";\n for (let p in type) {\n if (!type.hasOwnProperty(p)) continue;\n if (sPost) sPost += \"&\";\n sPost += p + '=' + encodeURIComponent(type[p]);\n }\n sPost = sPost.replace(/%20/g, '+');\n if (MAXDEBUG) Web.log(\"Web.getResource(POST \" + sURLRedirect + \"): \" + sPost.length + \" bytes\");\n request.open(\"POST\", sURLRedirect, fAsync);\n request.setRequestHeader(\"Content-type\", \"application/x-www-form-urlencoded\");\n request.send(sPost);\n } else {\n if (MAXDEBUG) Web.log(\"Web.getResource(GET \" + sURLRedirect + \")\");\n request.open(\"GET\", sURLRedirect, fAsync);\n if (type == \"arraybuffer\") {\n if (fXHR2) {\n fArrayBuffer = true;\n request.responseType = type;\n } else {\n request.overrideMimeType(\"text/plain; charset=x-user-defined\");\n }\n }\n request.send();\n }\n\n if (!fAsync) {\n request.readyState = 4; // this may already be set for synchronous requests, but I don't want to take any chances\n response = callback();\n }\n return response;\n }\n\n /**\n * parseMemoryResource(sURL, sData)\n *\n * This converts a variety of JSON-style data streams into an Object with the following properties:\n *\n * aBytes\n * aSymbols\n * addrLoad\n * addrExec\n *\n * If the source data contains a 'bytes' array, it's passed through to 'aBytes'; alternatively, if\n * it contains a 'words' array, the values are converted from 16-bit to 8-bit and stored in 'aBytes',\n * and if it contains a 'longs' array, the values are converted from 32-bit longs into bytes and\n * stored in 'aBytes'.\n *\n * Alternatively, if the source data contains a 'data' array, we simply pass that through to the output\n * object as:\n *\n * aData\n *\n * @param {string} sURL\n * @param {string} sData\n * @return {Object|null} (resource)\n */\n static parseMemoryResource(sURL, sData)\n {\n let i;\n let resource = {\n aBytes: null,\n aSymbols: null,\n addrLoad: null,\n addrExec: null\n };\n\n if (sData.charAt(0) == \"[\" || sData.charAt(0) == \"{\") {\n try {\n let a, ib, data;\n\n if (sData.substr(0, 1) == \"<\") { // if the \"data\" begins with a \"<\"...\n /*\n * Early server configs reported an error (via the nErrorCode parameter) if a tape URL was invalid,\n * but more recent server configs now display a somewhat friendlier HTML error page. The downside,\n * however, is that the original error has been buried, and we've received \"data\" that isn't actually\n * tape data. So if the data we've received appears to be \"HTML-like\", we treat it as an error message.\n */\n throw new Error(sData);\n }\n\n /*\n * TODO: IE9 is rather unfriendly and restrictive with regard to how much data it's willing to\n * eval(). In particular, the 10Mb disk image we use for the Windows 1.01 demo config fails in\n * IE9 with an \"Out of memory\" exception. One work-around would be to chop the data into chunks\n * (perhaps one track per chunk, using regular expressions) and then manually re-assemble it.\n *\n * However, it turns out that using JSON.parse(sDiskData) instead of eval(\"(\" + sDiskData + \")\")\n * is a much easier fix. The only drawback is that we must first quote any unquoted property names\n * and remove any comments, because while eval() was cool with them, JSON.parse() is more particular;\n * the following RegExp replacements take care of those requirements.\n *\n * The use of hex values is something else that eval() was OK with, but JSON.parse() is not, and\n * while I've stopped using hex values in DumpAPI responses (at least when \"format=json\" is specified),\n * I can't guarantee they won't show up in \"legacy\" images, and there's no simple RegExp replacement\n * for transforming hex values into decimal values, so I cop out and fall back to eval() if I detect\n * any hex prefixes (\"0x\") in the sequence. Ditto for error messages, which appear like so:\n *\n * [\"unrecognized disk path: test.img\"]\n */\n if (sData.indexOf(\"0x\") < 0 && sData.indexOf(\"0o\") < 0 && sData.substr(0, 2) != '[\"') {\n data = JSON.parse(sData.replace(/([a-z]+):/gm, '\"$1\":').replace(/\\/\\/[^\\n]*/gm, \"\"));\n } else {\n data = eval(\"(\" + sData + \")\");\n }\n\n resource.addrLoad = data['load'];\n resource.addrExec = data['exec'];\n\n if ((a = data['bytes'])) {\n resource.aBytes = a;\n }\n else if ((a = data['words'])) {\n /*\n * Convert all words into bytes\n */\n resource.aBytes = new Array(a.length * 2);\n for (i = 0, ib = 0; i < a.length; i++) {\n resource.aBytes[ib++] = a[i] & 0xff;\n resource.aBytes[ib++] = (a[i] >> 8) & 0xff;\n\n }\n }\n else if ((a = data['longs'])) {\n /*\n * Convert all dwords (longs) into bytes\n */\n resource.aBytes = new Array(a.length * 4);\n for (i = 0, ib = 0; i < a.length; i++) {\n resource.aBytes[ib++] = a[i] & 0xff;\n resource.aBytes[ib++] = (a[i] >> 8) & 0xff;\n resource.aBytes[ib++] = (a[i] >> 16) & 0xff;\n resource.aBytes[ib++] = (a[i] >> 24) & 0xff;\n }\n }\n else if ((a = data['data'])) {\n resource.aData = a;\n }\n else {\n resource.aBytes = data;\n }\n\n if (resource.aBytes) {\n if (!resource.aBytes.length) {\n Component.error(\"Empty resource: \" + sURL);\n resource = null;\n }\n else if (resource.aBytes.length == 1) {\n Component.error(resource.aBytes[0]);\n resource = null;\n }\n }\n resource.aSymbols = data['symbols'];\n\n } catch (e) {\n Component.error(\"Resource data error (\" + sURL + \"): \" + e.message);\n resource = null;\n }\n }\n else {\n /*\n * Parse the data manually; we assume it's a series of hex byte-values separated by whitespace.\n */\n let ab = [];\n let sHexData = sData.replace(/\\n/gm, \" \").replace(/ +$/, \"\");\n let asHexData = sHexData.split(\" \");\n for (i = 0; i < asHexData.length; i++) {\n let n = parseInt(asHexData[i], 16);\n if (isNaN(n)) {\n Component.error(\"Resource data error (\" + sURL + \"): invalid hex byte (\" + asHexData[i] + \")\");\n break;\n }\n ab.push(n & 0xff);\n }\n if (i == asHexData.length) resource.aBytes = ab;\n }\n return resource;\n }\n\n /**\n * redirectResource(sPath)\n *\n * The following replacements should only be necessary for (old) saved states, since all our disk manifests\n * should no longer be using any of these old paths.\n *\n * @param {string} sPath\n * @return {string}\n */\n static redirectResource(sPath)\n {\n sPath = sPath.replace(\"/disks/pc/\", \"/disks/pcx86/\");\n sPath = sPath.replace(\"/disks/pcx86/private/\", \"/disks-private/pcx86/\");\n if (sPath.indexOf(\"archive.pcjs.org\") < 0) {\n sPath = sPath.replace(\"/disks/pcx86/\", \"/disks-demo/pcx86/\");\n }\n sPath = sPath.replace(\"/pcjs-disks/\", \"/disks-demo/\");\n sPath = sPath.replace(\"/pcjs-games/\", \"/disks-game/\");\n sPath = sPath.replace(\"/disks-demo/pcx86/games/\", \"/disks-game/pcx86/\");\n sPath = sPath.replace(\"/private-disks/\", \"/disks-private/\");\n sPath = sPath.replace(\"/fixed/\", \"/drives/\");\n return sPath;\n }\n\n /**\n * sendReport(sApp, sVer, sURL, sUser, sType, sReport, sHostName)\n *\n * Send a report (eg, bug report) to the server.\n *\n * @param {string} sApp (eg, \"PCjs\")\n * @param {string} sVer (eg, \"1.02\")\n * @param {string} sURL (eg, \"/devices/pc/machine/5150/mda/64kb/machine.xml\")\n * @param {string} sUser (ie, the user key, if any)\n * @param {string} sType (eg, \"bug\"); one of ReportAPI.TYPE.*\n * @param {string} sReport (eg, unparsed state data)\n * @param {string} [sHostName] (default is SITEURL)\n */\n static sendReport(sApp, sVer, sURL, sUser, sType, sReport, sHostName)\n {\n let dataPost = {};\n dataPost[ReportAPI.QUERY.APP] = sApp;\n dataPost[ReportAPI.QUERY.VER] = sVer;\n dataPost[ReportAPI.QUERY.URL] = sURL;\n dataPost[ReportAPI.QUERY.USER] = sUser;\n dataPost[ReportAPI.QUERY.TYPE] = sType;\n dataPost[ReportAPI.QUERY.DATA] = sReport;\n let sReportURL = (sHostName? sHostName : SITEURL) + ReportAPI.ENDPOINT;\n Web.getResource(sReportURL, dataPost, true);\n }\n\n /**\n * getHost()\n *\n * This is like getHostName() but with the port number, if any.\n *\n * @return {string}\n */\n static getHost()\n {\n return (window? window.location.host : \"localhost\");\n }\n\n /**\n * getHostName()\n *\n * @return {string}\n */\n static getHostName()\n {\n return (window? window.location.hostname : \"localhost\");\n }\n\n /**\n * getHostOrigin()\n *\n * This could also be implemented with window.location.origin, but that wasn't originally available in all browsers.\n *\n * @return {string}\n */\n static getHostOrigin()\n {\n return (window? window.location.protocol + \"//\" + window.location.host : SITEURL);\n }\n\n /**\n * getHostProtocol()\n *\n * @return {string}\n */\n static getHostProtocol()\n {\n return (window? window.location.protocol : \"file:\");\n }\n\n /**\n * getHostURL()\n *\n * @return {string|null}\n */\n static getHostURL()\n {\n return (window? window.location.href : null);\n }\n\n /**\n * getUserAgent()\n *\n * @return {string}\n */\n static getUserAgent()\n {\n return (window? window.navigator.userAgent : \"\");\n }\n\n /**\n * hasLocalStorage\n *\n * true if localStorage support exists, is enabled, and works; false otherwise\n *\n * @return {boolean}\n */\n static hasLocalStorage()\n {\n if (Web.fLocalStorage == null) {\n let f = false;\n if (window) {\n try {\n window.localStorage.setItem(Web.sLocalStorageTest, Web.sLocalStorageTest);\n f = (window.localStorage.getItem(Web.sLocalStorageTest) == Web.sLocalStorageTest);\n window.localStorage.removeItem(Web.sLocalStorageTest);\n } catch (e) {\n Web.logLocalStorageError(e);\n f = false;\n }\n }\n Web.fLocalStorage = f;\n }\n return Web.fLocalStorage;\n }\n\n /**\n * logLocalStorageError(e)\n *\n * @param {Error} e is an exception\n */\n static logLocalStorageError(e)\n {\n Web.log(e.message, \"localStorage error\");\n }\n\n /**\n * getLocalStorageItem(sKey)\n *\n * Returns the requested key value, or null if the key does not exist, or undefined if localStorage is not available\n *\n * @param {string} sKey\n * @return {string|null|undefined} sValue\n */\n static getLocalStorageItem(sKey)\n {\n let sValue;\n if (window) {\n try {\n sValue = window.localStorage.getItem(sKey);\n } catch (e) {\n Web.logLocalStorageError(e);\n }\n }\n return sValue;\n }\n\n /**\n * setLocalStorageItem(sKey, sValue)\n *\n * @param {string} sKey\n * @param {string} sValue\n * @return {boolean} true if localStorage is available, false if not\n */\n static setLocalStorageItem(sKey, sValue)\n {\n try {\n window.localStorage.setItem(sKey, sValue);\n return true;\n } catch (e) {\n Web.logLocalStorageError(e);\n }\n return false;\n }\n\n /**\n * removeLocalStorageItem(sKey)\n *\n * @param {string} sKey\n */\n static removeLocalStorageItem(sKey)\n {\n try {\n window.localStorage.removeItem(sKey);\n } catch (e) {\n Web.logLocalStorageError(e);\n }\n }\n\n /**\n * getLocalStorageKeys()\n *\n * @return {Array}\n */\n static getLocalStorageKeys()\n {\n let a = [];\n try {\n for (let i = 0, c = window.localStorage.length; i < c; i++) {\n a.push(window.localStorage.key(i));\n }\n } catch (e) {\n Web.logLocalStorageError(e);\n }\n return a;\n }\n\n /**\n * reloadPage()\n */\n static reloadPage()\n {\n if (window) window.location.reload();\n }\n\n /**\n * isUserAgent(s)\n *\n * Check the browser's user-agent string for the given substring; \"iOS\" and \"MSIE\" are special values you can\n * use that will match any iOS or MSIE browser, respectively (even IE11, in the case of \"MSIE\").\n *\n * 2013-11-06: In a questionable move, MSFT changed the user-agent reported by IE11 on Windows 8.1, eliminating\n * the \"MSIE\" string (which MSDN calls a \"version token\"; see http://msdn.microsoft.com/library/ms537503.aspx);\n * they say \"public websites should rely on feature detection, rather than browser detection, in order to design\n * their sites for browsers that don't support the features used by the website.\" So, in IE11, we get a user-agent\n * that tries to fool apps into thinking the browser is more like WebKit or Gecko:\n *\n * Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko\n *\n * That's a nice idea, but in the meantime, they hosed the XSL transform code in embed.js, which contained\n * some very critical browser-specific code; turning on IE's \"Compatibility Mode\" didn't help either, because\n * that's a sledgehammer solution which restores the old user-agent string but also disables other features like\n * HTML5 canvas support. As an interim solution, I'm treating any \"MSIE\" check as a check for either \"MSIE\" or\n * \"Trident\".\n *\n * UPDATE: I've since found ways to make the code in embed.js more browser-agnostic, so for now, there's isn't\n * any code that cares about \"MSIE\", but I've left the change in place, because I wouldn't be surprised if I'll\n * need more IE-specific code in the future, perhaps for things like copy/paste functionality, or mouse capture.\n *\n * @param {string} s is a substring to search for in the user-agent; as noted above, \"iOS\" and \"MSIE\" are special values\n * @return {boolean} is true if the string was found, false if not\n */\n static isUserAgent(s)\n {\n if (window) {\n let userAgent = Web.getUserAgent();\n /*\n * Here's one case where we have to be careful with Component, because when isUserAgent() is called by\n * the init code below, component.js hasn't been loaded yet. The simple solution for now is to remove the call.\n *\n * Web.log(\"agent: \" + userAgent);\n *\n * And yes, it would be pointless to use the conditional (?) operator below, if not for the Google Closure\n * Compiler (v20130823) failing to detect the entire expression as a boolean.\n */\n return s == \"iOS\" && !!userAgent.match(/(iPod|iPhone|iPad)/) && !!userAgent.match(/AppleWebKit/) || s == \"MSIE\" && !!userAgent.match(/(MSIE|Trident)/) || (userAgent.indexOf(s) >= 0);\n }\n return false;\n }\n\n /**\n * isMobile(sDevice)\n *\n * Checks the URL for a \"mobile\" parameter, and failing that, checks the browser's user-agent string for the\n * substring \"Mobi\", as per Mozilla recommendation:\n *\n * https://developer.mozilla.org/en-US/docs/Browser_detection_using_the_user_agent\n *\n * @param {string} [sDevice] (eg, \"iPad\" to check for iPad, or \"!iPad\" to specifically exclude it)\n * @return {boolean} is true if the browser appears to be a mobile (ie, non-desktop) web browser, false if not\n */\n static isMobile(sDevice)\n {\n let sMobile = Web.getURLParm(\"mobile\");\n if (sMobile) return sMobile == \"true\";\n if (Web.isUserAgent(\"Mobi\")) {\n if (!sDevice) return true;\n let fInvert = sDevice[0] == '!';\n if (fInvert) sDevice = sDevice.substr(1);\n return Web.isUserAgent(sDevice) != fInvert;\n }\n return false;\n }\n\n /**\n * findProperty(obj, sProp, sSuffix)\n *\n * If both sProp and sSuffix are set, then any browser-specific prefixes are inserted between sProp and sSuffix,\n * and if a match is found, it is returned without sProp.\n *\n * For example, if findProperty(document, 'on', 'fullscreenchange') discovers that 'onwebkitfullscreenchange' exists,\n * it will return 'webkitfullscreenchange', in preparation for an addEventListener() call.\n *\n * More commonly, sSuffix is not used, so whatever property is found is returned as-is.\n *\n * @param {Object|null|undefined} obj\n * @param {string} sProp\n * @param {string} [sSuffix]\n * @return {string|null}\n */\n static findProperty(obj, sProp, sSuffix)\n {\n if (obj) {\n for (let i = 0; i < Web.asBrowserPrefixes.length; i++) {\n let sName = Web.asBrowserPrefixes[i];\n if (sSuffix) {\n sName += sSuffix;\n let sEvent = sProp + sName;\n if (sEvent in obj) return sName;\n } else {\n if (!sName) {\n sName = sProp[0];\n } else {\n sName += sProp[0].toUpperCase();\n }\n sName += sProp.substr(1);\n if (sName in obj) return sName;\n }\n }\n }\n return null;\n }\n\n /**\n * getURLParm(sParm)\n *\n * First looks for sParm exactly as specified, then looks for the lower-case version.\n *\n * @param {string} sParm\n * @return {string|undefined}\n */\n static getURLParm(sParm)\n {\n if (!Web.parmsURL) {\n Web.parmsURL = Web.parseURLParms();\n }\n return Web.parmsURL[sParm] || Web.parmsURL[sParm.toLowerCase()];\n }\n\n /**\n * parseURLParms(sParms)\n *\n * @param {string} [sParms] containing the parameter portion of a URL (ie, after the '?')\n * @return {Object} containing properties for each parameter found\n */\n static parseURLParms(sParms)\n {\n let aParms = {};\n if (window) { // an alternative to \"if (typeof module === 'undefined')\" if require(\"defines\") was used\n if (!sParms) {\n /*\n * Note that window.location.href returns the entire URL, whereas window.location.search\n * returns only the parameters, if any (starting with the '?', which we skip over with a substr() call).\n */\n sParms = window.location.search.substr(1);\n }\n let match;\n let pl = /\\+/g; // RegExp for replacing addition symbol with a space\n let search = /([^&=]+)=?([^&]*)/g;\n let decode = function(s)\n {\n return decodeURIComponent(s.replace(pl, \" \"));\n };\n\n while ((match = search.exec(sParms))) {\n aParms[decode(match[1])] = decode(match[2]);\n }\n }\n return aParms;\n }\n\n /**\n * downloadFile(sData, sType, fBase64, sFileName)\n *\n * @param {string|Uint8Array} sData\n * @param {string} sType\n * @param {boolean} [fBase64]\n * @param {string} [sFileName]\n */\n static downloadFile(sData, sType, fBase64, sFileName)\n {\n let link = null, sAlert, sURI;\n\n if (typeof sData != 'string') {\n if (typeof Blob == 'function' && typeof URL != 'undefined' && URL && typeof URL.createObjectURL == 'function') {\n let blob = new Blob([sData], {type: 'application/octet-stream'});\n sURI = URL.createObjectURL(blob);\n }\n }\n else {\n sURI = \"data:application/\" + sType + (fBase64? \";base64\" : \"\") + \",\";\n if (!Web.isUserAgent(\"Firefox\")) {\n sURI += (fBase64? sData : encodeURI(sData));\n } else {\n sURI += (fBase64? sData : encodeURIComponent(sData));\n }\n }\n if (!sURI) {\n sAlert = 'Operation unsupported by your browser.';\n }\n else {\n if (sFileName) {\n link = document.createElement('a');\n if (typeof link.download != 'string') link = null;\n }\n if (link) {\n link.href = sURI;\n link.download = sFileName;\n document.body.appendChild(link); // Firefox allegedly requires the link to be in the body\n link.click();\n document.body.removeChild(link);\n sAlert = 'Check your Downloads folder for ' + sFileName + '.';\n // if (Web.isUserAgent(\"Chrome\")) {\n // sAlert += '\\n\\nIn Chrome, after clicking OK, you may ALSO have to select the \"Window\" menu, choose \"Downloads\", and then locate this download and select \"Keep\".';\n // sAlert += '\\n\\nThis is part of Chrome\\'s \"Security By Jumping Through Extra Hoops\" technology, which is much easier for Google to implement than actually checking for something malicious.';\n // sAlert += '\\n\\nAnd for the record, there is nothing malicious on the PCjs website.';\n // }\n }\n else {\n window.open(sURI);\n sAlert = 'Check your browser for a new window/tab containing the requested data' + (sFileName? (' (' + sFileName + ')') : '') + '.';\n }\n }\n return sAlert;\n }\n\n /**\n * onCountRepeat(n, fnRepeat, fnComplete, msDelay)\n *\n * Call fnRepeat() n times with an msDelay millisecond delay between calls,\n * then call fnComplete() when n has been exhausted OR fnRepeat() returns false.\n *\n * @param {number} n\n * @param {function()} fnRepeat\n * @param {function()} fnComplete\n * @param {number} [msDelay]\n */\n static onCountRepeat(n, fnRepeat, fnComplete, msDelay)\n {\n let fnTimeout = function doCountRepeat()\n {\n n -= 1;\n if (n >= 0) {\n if (!fnRepeat()) n = 0;\n }\n if (n > 0) {\n setTimeout(fnTimeout, msDelay || 0);\n return;\n }\n fnComplete();\n };\n fnTimeout();\n }\n\n /**\n * onClickRepeat(e, msDelay, msRepeat, fn)\n *\n * Repeatedly call fn() with an initial msDelay, and an msRepeat delay thereafter,\n * as long as HTML control Object e has an active \"down\" event and fn() returns true.\n *\n * @param {Object} e\n * @param {number} msDelay\n * @param {number} msRepeat\n * @param {function(boolean)} fn is passed false on the first call, true on all repeated calls\n */\n static onClickRepeat(e, msDelay, msRepeat, fn)\n {\n let ms = 0, timer = null, fIgnoreMouseEvents = false;\n\n let fnRepeat = function doClickRepeat()\n {\n if (fn(ms === msRepeat)) {\n timer = setTimeout(fnRepeat, ms);\n ms = msRepeat;\n }\n };\n e.onmousedown = function()\n {\n // Web.log(\"onMouseDown()\");\n if (!fIgnoreMouseEvents) {\n if (!timer) {\n ms = msDelay;\n fnRepeat();\n }\n }\n };\n e.ontouchstart = function()\n {\n // Web.log(\"onTouchStart()\");\n if (!timer) {\n ms = msDelay;\n fnRepeat();\n }\n };\n e.onmouseup = e.onmouseout = function()\n {\n // Web.log(\"onMouseUp()/onMouseOut()\");\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n };\n e.ontouchend = e.ontouchcancel = function()\n {\n // Web.log(\"onTouchEnd()/onTouchCancel()\");\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n /*\n * Devices that generate ontouch* events ALSO generate onmouse* events,\n * and generally do so immediately after all the touch events are complete,\n * so unless we want double the action, we need to ignore mouse events.\n */\n fIgnoreMouseEvents = true;\n };\n }\n\n /**\n * onPageEvent(sName, fn)\n *\n * For 'onload', 'onunload', and 'onpageshow' events, most callers should NOT use this function, but\n * instead use Web.onInit(), Web.onShow(), and Web.onExit(), respectively.\n *\n * The only components that should still use onPageEvent() are THIS component (see the bottom of this file)\n * and components that need to capture other events (eg, the 'onresize' event in the Video component).\n *\n * This function creates a chain of callbacks, allowing multiple JavaScript modules to define handlers\n * for the same event, which wouldn't be possible if everyone modified window['onload'], window['onunload'],\n * etc, themselves. However, that's less of a concern now, because assuming everyone else is now using\n * onInit(), onExit(), etc, then there really IS only one component setting the window callback: this one.\n *\n * NOTE: It's risky to refer to obscure event handlers with \"dot\" names, because the Closure Compiler may\n * erroneously replace them (eg, window.onpageshow is a good example).\n *\n * @param {string} sFunc\n * @param {function()} fn\n */\n static onPageEvent(sFunc, fn)\n {\n if (window) {\n let fnPrev = window[sFunc];\n if (typeof fnPrev !== 'function') {\n window[sFunc] = fn;\n } else {\n /*\n * TODO: Determine whether there's any value in receiving/sending the Event object that the\n * browser provides when it generates the original event.\n */\n window[sFunc] = function onWindowEvent()\n {\n if (fnPrev) fnPrev();\n fn();\n };\n }\n }\n }\n\n /**\n * onInit(fn)\n *\n * Use this instead of setting window.onload. Allows multiple JavaScript modules to define their own 'onload' event handler.\n *\n * @param {function()} fn\n */\n static onInit(fn)\n {\n Web.aPageEventHandlers['init'].push(fn);\n }\n\n /**\n * onShow(fn)\n *\n * @param {function()} fn\n *\n * Use this instead of setting window.onpageshow. Allows multiple JavaScript modules to define their own 'onpageshow' event handler.\n */\n static onShow(fn)\n {\n Web.aPageEventHandlers['show'].push(fn);\n }\n\n /**\n * onError(sMessage)\n *\n * @param {string} sMessage\n */\n static onError(sMessage)\n {\n Web.notice(sMessage + \"\\n\\nIf it happens again, please send the URL to support@pcjs.org. Thanks.\");\n }\n\n /**\n * onExit(fn)\n *\n * @param {function()} fn\n *\n * Use this instead of setting window.onunload. Allows multiple JavaScript modules to define their own 'onunload' event handler.\n */\n static onExit(fn)\n {\n Web.aPageEventHandlers['exit'].push(fn);\n }\n\n /**\n * doPageEvent(afn)\n *\n * @param {Array.} afn\n */\n static doPageEvent(afn)\n {\n if (Web.fPageEventsEnabled) {\n try {\n for (let i = 0; i < afn.length; i++) {\n afn[i]();\n }\n } catch (e) {\n Web.onError(\"An unexpected error occurred: \" + e.message);\n }\n }\n }\n\n /**\n * enablePageEvents(fEnable)\n *\n * @param {boolean} fEnable is true to enable page events, false to disable (they're enabled by default)\n */\n static enablePageEvents(fEnable)\n {\n if (!Web.fPageEventsEnabled && fEnable) {\n Web.fPageEventsEnabled = true;\n if (Web.fPageLoaded) Web.sendPageEvent('init');\n if (Web.fPageShowed) Web.sendPageEvent('show');\n return;\n }\n Web.fPageEventsEnabled = fEnable;\n }\n\n /**\n * sendPageEvent(sEvent)\n *\n * This allows us to manually trigger page events.\n *\n * @param {string} sEvent (one of 'init', 'show' or 'exit')\n */\n static sendPageEvent(sEvent)\n {\n if (Web.aPageEventHandlers[sEvent]) {\n Web.doPageEvent(Web.aPageEventHandlers[sEvent]);\n }\n }\n}\n\nWeb.parmsURL = null; // initialized on first call to parseURLParms()\n\nWeb.aPageEventHandlers = {\n 'init': [], // list of window 'onload' handlers\n 'show': [], // list of window 'onpageshow' handlers\n 'exit': [] // list of window 'onunload' handlers (although we prefer to use 'onbeforeunload' if possible)\n};\n\nWeb.asBrowserPrefixes = ['', 'moz', 'ms', 'webkit'];\n\nWeb.fPageLoaded = false; // set once the page's first 'onload' event has occurred\nWeb.fPageShowed = false; // set once the page's first 'onpageshow' event has occurred\nWeb.fPageEventsEnabled = true; // default is true, set to false (or true) by enablePageEvents()\nWeb.fAdBlockerWarning = false;\n\n/**\n * fLocalStorage\n *\n * true if localStorage support exists, is enabled, and works; \"falsey\" otherwise\n *\n * @type {boolean|null}\n */\nWeb.fLocalStorage = null;\n\n/**\n * TODO: Is there any way to get the Closure Compiler to stop inlining this string? This isn't cutting it.\n *\n * @const {string}\n */\nWeb.sLocalStorageTest = \"PCjs.localStorage\";\n\nWeb.onPageEvent('onload', function onPageLoad() {\n Web.fPageLoaded = true;\n Web.doPageEvent(Web.aPageEventHandlers['init']);\n});\n\nWeb.onPageEvent('onpageshow', function onPageShow() {\n Web.fPageShowed = true;\n Web.doPageEvent(Web.aPageEventHandlers['show']);\n});\n\nWeb.onPageEvent(Web.isUserAgent(\"iOS\")? 'onpagehide' : (Web.isUserAgent(\"Opera\")? 'onunload' : 'onbeforeunload'), function onPageUnload() {\n Web.doPageEvent(Web.aPageEventHandlers['exit']);\n});\n\n/*\n * If this is DEBUG (eg, un-COMPILED) code, then allow the user to override DEBUG with a \"debug=false\" embedded in\n * the URL; note that the Closure Compiler won't let us alter the DEBUG variable, because it's defined as a @define, which\n * implies @const as well, so we must resort to modifying it indirectly, using the global window object.\n *\n * TODO: Consider yet another embedXXX() parameter that would also allow DEBUG to be turned off on a page-by-page basis;\n * it's low priority, because it would only affect machines that explicitly request un-COMPILED code, and there are very\n * few such machines (eg, /_posts/2015-01-17-pcjs-uncompiled.md).\n *\n * Deal with Web.getURLParm(\"backtrack\") in /modules/pcx86/lib/defines.js at the same time.\n */\nif (DEBUG && window) {\n let sDebug = Web.getURLParm(\"debug\");\n if (sDebug == \"false\") {\n window['DEBUG'] = false;\n }\n}\n\n\n\n/**\n * @copyright https://www.pcjs.org/modules/shared/lib/component.js (C) Jeff Parsons 2012-2019\n */\n\n/*\n * All PCjs components now use JSDoc types, primarily so that Google's Closure Compiler will compile\n * everything with zero warnings when ADVANCED_OPTIMIZATIONS are enabled. For more information about\n * the JSDoc types supported by the Closure Compiler:\n *\n * https://developers.google.com/closure/compiler/docs/js-for-compiler#types\n *\n * I also attempted to validate this code with JSLint, but it complained too much; eg, it didn't like\n * \"while (true)\", a tried and \"true\" programming convention for decades, and it wanted me to replace\n * all \"++\" and \"--\" operators with \"+= 1\" and \"-= 1\", use \"(s || '')\" instead of \"(s? s : '')\", etc.\n *\n * I prefer sticking with traditional C-style idioms, in part because they are more portable. That\n * does NOT mean I'm trying to write \"portable JavaScript,\" but some of this code was ported from C code\n * I'd written long ago, so portability is good, and I'm not going to throw that away if there's no need.\n *\n * UPDATE: I've since switched from JSLint to JSHint, which seems to have more reasonable defaults.\n * And for new code, I have adopted some popular JavaScript idioms, like \"(s || '')\", although the need\n * for those kinds of expressions will be reduced as I also start adopting some ES6 features, like\n * default parameters.\n */\n\n\n\n/**\n * Since the Closure Compiler treats ES6 classes as @struct rather than @dict by default,\n * it deters us from defining named properties on our components; eg:\n *\n * this['exports'] = {...}\n *\n * results in an error:\n *\n * Cannot do '[]' access on a struct\n *\n * So, in order to define 'exports', we must override the @struct assumption by annotating\n * the class as @unrestricted (or @dict). Note that this must be done both here and in the\n * subclass (eg, SerialPort), because otherwise the Compiler won't allow us to *reference*\n * the named property either.\n *\n * TODO: Consider marking ALL our classes unrestricted, because otherwise it forces us to\n * define every single property the class uses in its constructor, which results in a fair\n * bit of redundant initialization, since many properties aren't (and don't need to be) fully\n * initialized until the appropriate init(), reset(), restore(), etc. function is called.\n *\n * The upside, however, may be that since the structure of the class is completely defined by\n * the constructor, JavaScript engines may be able to optimize and run more efficiently.\n *\n * @unrestricted\n */\nclass Component {\n /**\n * Component(type, parms, bitsMessage)\n *\n * A Component object requires:\n *\n * type: a user-defined type name (eg, \"CPU\")\n *\n * and accepts any or all of the following (parms) properties:\n *\n * id: component ID (default is \"\")\n * name: component name (default is \"\"; if blank, toString() will use the type name only)\n * comment: component comment string (default is undefined)\n *\n * Component subclasses will usually have additional (parms) properties.\n *\n * @param {string} type\n * @param {Object} [parms]\n * @param {number} [bitsMessage] selects message(s) that the component wants to enable (default is 0)\n */\n constructor(type, parms, bitsMessage)\n {\n this.type = type;\n\n if (!parms) parms = {'id': \"\", 'name': \"\"};\n\n this.id = parms['id'] || \"\";\n this.name = parms['name'];\n this.comment = parms['comment'];\n this.parms = parms;\n\n /*\n * The following Component properties need to be accessible by other machines and/or command scripts;\n * well, OK, or we could have exported some new functions to walk the contents of these properties, as we\n * did with findMachineComponent(), but this works just as well.\n *\n * Also, while the double-assignment looks silly (ie, using both dot and bracket property notation), it\n * resolves a complaint from the Closure Compiler, because if we use ONLY bracket notation here, then the\n * Compiler wants us to change all the other references to bracket notation as well.\n */\n this.exports = this['exports'] = {};\n this.bindings = this['bindings'] = {};\n\n let i = this.id.indexOf('.');\n if (i < 0) {\n this.idMachine = \"PCjs\";\n this.idComponent = this.id;\n } else {\n this.idMachine = this.id.substr(0, i);\n this.idComponent = this.id.substr(i + 1);\n }\n\n /*\n * Gather all the various component flags (booleans) into a single \"flags\" object, and encourage\n * subclasses to do the same, to reduce the property clutter we have to wade through while debugging.\n */\n this.flags = {\n ready: false,\n busy: false,\n busyCancel: false,\n initDone: false,\n powered: false,\n unloading: false,\n error: false\n };\n\n this.fnReady = null;\n this.clearError();\n this.bitsMessage = bitsMessage || 0;\n\n this.cmp = null;\n this.bus = null;\n this.cpu = null;\n this.dbg = null;\n\n /*\n * TODO: Consider adding another parameter to the Component() constructor that allows components to tell\n * us if they support single or multiple instances per machine. For example, there can be multiple SerialPort\n * components per machine, but only one CPU component (some machines also support an FPU, but that component\n * is considered separate from the CPU).\n *\n * It's not critical, but it would help catch machine configuration errors; for example, a machine that mistakenly\n * includes two CPU components may, aside from wasting memory, end up with odd side-effects, like unresponsive\n * CPU controls.\n */\n Component.add(this);\n }\n\n /**\n * Component.add(component)\n *\n * @param {Component} component\n */\n static add(component)\n {\n /*\n * This just generates a lot of useless noise, handy in the early days, not so much these days....\n *\n * if (DEBUG) Component.log(\"Component.add(\" + component.type + \",\" + component.id + \")\");\n */\n Component.components.push(component);\n }\n\n /**\n * Component.addMachine(idMachine)\n *\n * @param {string} idMachine\n */\n static addMachine(idMachine)\n {\n Component.machines[idMachine] = {};\n }\n\n /**\n * Component.getMachines()\n *\n * @return {Array.}\n */\n static getMachines()\n {\n return Object.keys(Component.machines);\n }\n\n /**\n * Component.addMachineResource(idMachine, sName, data)\n *\n * @param {string} idMachine\n * @param {string|null} sName (name of the resource)\n * @param {*} data\n */\n static addMachineResource(idMachine, sName, data)\n {\n /*\n * I used to assert(Component.machines[idMachine]), but when we're running as a Node app, embed.js is not used,\n * so addMachine() is never called, so resources do not need to be recorded.\n */\n if (Component.machines[idMachine] && sName) {\n Component.machines[idMachine][sName] = data;\n }\n }\n\n /**\n * Component.getMachineResources(idMachine)\n *\n * @param {string} idMachine\n * @return {Object|undefined}\n */\n static getMachineResources(idMachine)\n {\n return Component.machines[idMachine];\n }\n\n /**\n * Component.getTime()\n *\n * @return {number} the current time, in milliseconds\n */\n static getTime()\n {\n return Date.now() || +new Date();\n }\n\n /**\n * Component.log(s, type)\n *\n * For diagnostic output only.\n *\n * @param {string} [s] is the message text\n * @param {string} [type] is the message type\n */\n static log(s, type)\n {\n if (!COMPILED) {\n if (s) {\n let sElapsed = \"\", sMsg = (type? (type + \": \") : \"\") + s;\n if (typeof Usr != \"undefined\") {\n if (Component.msStart === undefined) {\n Component.msStart = Component.getTime();\n }\n sElapsed = (Component.getTime() - Component.msStart) + \"ms: \";\n }\n sMsg = sMsg.replace(/\\r/g, '\\\\r').replace(/\\n/g, ' ');\n if (window && window.console) console.log(sElapsed + sMsg);\n }\n }\n }\n\n /**\n * Component.assert(f, s)\n *\n * Verifies conditions that must be true (for DEBUG builds only).\n *\n * The Closure Compiler should automatically remove all references to Component.assert() in non-DEBUG builds.\n * TODO: Add a task to the build process that \"asserts\" there are no instances of \"assertion failure\" in RELEASE builds.\n *\n * @param {boolean} f is the expression we are asserting to be true\n * @param {string} [s] is description of the assertion on failure\n */\n static assert(f, s)\n {\n if (DEBUG) {\n if (!f) {\n if (!s) s = \"assertion failure\";\n Component.log(s);\n throw new Error(s);\n }\n }\n }\n\n /**\n * Component.print(s)\n *\n * Components that inherit from this class should use this.print(), rather than Component.print(), because\n * if a Control Panel is loaded, it will override only the instance method, not the class method (overriding the\n * class method would improperly affect any other machines loaded on the same page).\n *\n * @this {Component}\n * @param {string} s\n */\n static print(s)\n {\n if (!COMPILED) {\n let i = s.lastIndexOf('\\n');\n if (i >= 0) {\n Component.println(s.substr(0, i));\n s = s.substr(i + 1);\n }\n Component.printBuffer += s;\n }\n }\n\n /**\n * Component.println(s, type, id)\n *\n * Components that inherit from this class should use this.println(), rather than Component.println(), because\n * if a Control Panel is loaded, it will override only the instance method, not the class method (overriding the\n * class method would improperly affect any other machines loaded on the same page).\n *\n * @param {string} [s] is the message text\n * @param {string} [type] is the message type\n * @param {string} [id] is the caller's ID, if any\n */\n static println(s, type, id)\n {\n if (!COMPILED) {\n s = Component.printBuffer + (s || \"\");\n Component.log((id? (id + \": \") : \"\") + (s? (\"\\\"\" + s + \"\\\"\") : \"\"), type);\n Component.printBuffer = \"\";\n }\n }\n\n /**\n * Component.notice(s, fPrintOnly, id)\n *\n * notice() is like println() but implies a need for user notification, so we alert() as well.\n *\n * @param {string} s is the message text\n * @param {boolean} [fPrintOnly]\n * @param {string} [id] is the caller's ID, if any\n * @return {boolean}\n */\n static notice(s, fPrintOnly, id)\n {\n if (!COMPILED) {\n Component.println(s, Component.PRINT.NOTICE, id);\n }\n if (!fPrintOnly) Component.alertUser((id? (id + \": \") : \"\") + s);\n return true;\n }\n\n /**\n * Component.warning(s)\n *\n * @param {string} s describes the warning\n */\n static warning(s)\n {\n if (!COMPILED) {\n Component.println(s, Component.PRINT.WARNING);\n }\n Component.alertUser(s);\n }\n\n /**\n * Component.error(s)\n *\n * @param {string} s describes the error; an alert() is displayed as well\n */\n static error(s)\n {\n if (!COMPILED) {\n Component.println(s, Component.PRINT.ERROR);\n }\n Component.alertUser(s);\n }\n\n /**\n * Component.alertUser(sMessage)\n *\n * @param {string} sMessage\n */\n static alertUser(sMessage)\n {\n if (window) {\n window.alert(sMessage);\n } else {\n Component.log(sMessage);\n }\n }\n\n /**\n * Component.confirmUser(sPrompt)\n *\n * @param {string} sPrompt\n * @returns {boolean} true if the user clicked OK, false if Cancel/Close\n */\n static confirmUser(sPrompt)\n {\n let fResponse = false;\n if (window) {\n fResponse = window.confirm(sPrompt);\n }\n return fResponse;\n }\n\n /**\n * Component.promptUser()\n *\n * @param {string} sPrompt\n * @param {string} [sDefault]\n * @returns {string|null}\n */\n static promptUser(sPrompt, sDefault)\n {\n let sResponse = null;\n if (window) {\n sResponse = window.prompt(sPrompt, sDefault === undefined? \"\" : sDefault);\n }\n return sResponse;\n }\n\n /**\n * Component.appendControl(control, sText)\n *\n * @param {Object} control\n * @param {string} sText\n */\n static appendControl(control, sText)\n {\n control.value += sText;\n /*\n * Prevent the