"use strict"; /** * @copyright http://pcjs.org/modules/shared/lib/defines.js (C) Jeff Parsons 2012-2017 */ /** * @define {string} */ var APPVERSION = "1.x.x"; // this @define is overridden by the Closure Compiler with the version in package.json var XMLVERSION = null; // this is set in non-COMPILED builds by embedMachine() if a version number was found in the machine XML var COPYRIGHT = "Copyright © 2012-2017 Jeff Parsons "; var LICENSE = "License: GPL version 3 or later "; var CSSCLASS = "pcjs"; /** * @define {string} */ var SITEHOST = "localhost:8088";// this @define is overridden by the Closure Compiler with "www.pcjs.org" /** * @define {boolean} */ var COMPILED = false; // this @define is overridden by the Closure Compiler (to true) /** * @define {boolean} */ var DEBUG = true; // this @define is overridden by the Closure Compiler (to false) to remove DEBUG-only code /** * @define {boolean} */ var MAXDEBUG = false; // this @define is overridden by the Closure Compiler (to false) to remove MAXDEBUG-only code /** * @define {boolean} */ var PRIVATE = false; // this @define is overridden by the Closure Compiler (to false) to enable PRIVATE code /* * RS-232 DB-25 Pin Definitions, mapped to bits 1-25 in a 32-bit status value. * * SerialPorts in PCjs machines are considered DTE (Data Terminal Equipment), which means they should be "virtually" * connected to each other via a null-modem cable, which assumes the following cross-wiring: * * G 1 <-> 1 G (Ground) * TD 2 <-> 3 RD (Received Data) * RD 3 <-> 2 TD (Transmitted Data) * RTS 4 <-> 5 CTS (Clear To Send) * CTS 5 <-> 4 RTS (Request To Send) * DSR 6+8 <-> 20 DTR (Data Terminal Ready) * SG 7 <-> 7 SG (Signal Ground) * DTR 20 <-> 6+8 DSR (Data Set Ready + Carrier Detect) * RI 22 <-> 22 RI (Ring Indicator) * * TODO: Move these definitions to a more appropriate shared file at some point. */ var RS232 = { RTS: { PIN: 4, MASK: 0x00000010 }, CTS: { PIN: 5, MASK: 0x00000020 }, DSR: { PIN: 6, MASK: 0x00000040 }, CD: { PIN: 8, MASK: 0x00000100 }, DTR: { PIN: 20, MASK: 0x00100000 }, RI: { PIN: 22, MASK: 0x00400000 } }; /* * NODE should be true if we're running under NodeJS (eg, command-line), false if not (eg, web browser) */ var NODE = false; /** * @copyright http://pcjs.org/modules/shared/lib/diskapi.js (C) Jeff Parsons 2012-2017 */ /* * Our "DiskIO API" looks like: * * http://www.pcjs.org/api/v1/disk?action=open&volume=*10mb.img&mode=demandrw&chs=c:h:s&machine=xxx&user=yyy */ var DiskAPI = { ENDPOINT: "/api/v1/disk", QUERY: { ACTION: "action", // value is one of DiskAPI.ACTION.* VOLUME: "volume", // value is path of a disk image MODE: "mode", // value is one of DiskAPI.MODE.* CHS: "chs", // value is cylinders:heads:sectors:bytes ADDR: "addr", // value is cylinder:head:sector:count MACHINE: "machine", // value is machine token USER: "user", // value is user ID DATA: "data" // value is data to be written }, ACTION: { OPEN: "open", READ: "read", WRITE: "write", CLOSE: "close" }, MODE: { LOCAL: "local", // this mode implies no API (at best, localStorage backing only) PRELOAD: "preload", // this mode implies use of the DumpAPI DEMANDRW: "demandrw", DEMANDRO: "demandro" }, FAIL: { BADACTION: "invalid action", BADUSER: "invalid user", BADVOL: "invalid volume", OPENVOL: "unable to open volume", CREATEVOL: "unable to create volume", WRITEVOL: "unable to write volume", REVOKED: "access revoked" } }; /* * TODO: Eventually, our tools will need to support looking up disk formats by "model" rather than by raw disk size, * because obviously multiple disk geometries can yield the same raw disk size. For each conflict that arises, I'll * probably create a fake (approximate) disk size entry above, and then create a mapping to that approximate size below. */ DiskAPI.MODELS = { "RL01": 5242880, "RL02": 10485760 }; DiskAPI.MBR = { PARTITIONS: { OFFSET: 0x1BE, ENTRY: { STATUS: 0x00, // 0x80 if active CHS_FIRST: 0x01, // 3-byte CHS specifier TYPE: 0x04, // see TYPE.* CHS_LAST: 0x05, // 3-byte CHS specifier LBA_FIRST: 0x08, LBA_TOTAL: 0x0C, LENGTH: 0x10 }, STATUS: { ACTIVE: 0x80 }, TYPE: { EMPTY: 0x00, FAT12_PRIMARY: 0x01, // DOS 2.0 and up (12-bit FAT) FAT16_PRIMARY: 0x04 // DOS 3.0 and up (16-bit FAT) } }, SIG_OFFSET: 0x1FE, SIGNATURE: 0xAA55 // to be clear, the low byte (at offset 0x1FE) is 0x55 and the high byte (at offset 0x1FF) is 0xAA }; /* * Boot sector offsets (and assorted constants) in DOS-compatible boot sectors (DOS 2.0 and up) * * WARNING: I've heard apocryphal stories about SIGNATURE being improperly reversed on some systems * (ie, 0x55AA instead 0xAA55) -- perhaps by a dyslexic programmer -- so be careful out there. */ DiskAPI.BOOT = { JMP_OPCODE: 0x000, // 1 byte for a JMP opcode, followed by a 1 or 2-byte offset OEM_STRING: 0x003, // 8 bytes SIG_OFFSET: 0x1FE, SIGNATURE: 0xAA55 // to be clear, the low byte (at offset 0x1FE) is 0x55 and the high byte (at offset 0x1FF) is 0xAA }; /* * BIOS Parameter Block (BPB) offsets in DOS-compatible boot sectors (DOS 2.x and up) * * NOTE: DOS 2.x OEM documentation says that the words starting at offset 0x018 (TRACK_SECS, TOTAL_HEADS, and HIDDEN_SECS) * are optional, but even the DOS 2.0 FORMAT utility initializes all three of those words. There may be some OEM media out * there with BPBs that are only valid up to offset 0x018, but I've not run across any media like that. * * DOS 3.20 added LARGE_SECS, but unfortunately, it was added as a 2-byte value at offset 0x01E. DOS 3.31 decided * to make both HIDDEN_SECS and LARGE_SECS 4-byte values, which meant that LARGE_SECS had to move from 0x01E to 0x020. */ DiskAPI.BPB = { SECTOR_BYTES: 0x00B, // 2 bytes: bytes per sector (eg, 0x200 or 512) CLUSTER_SECS: 0x00D, // 1 byte: sectors per cluster (eg, 1) RESERVED_SECS: 0x00E, // 2 bytes: reserved sectors; ie, # sectors preceding the first FAT--usually just the boot sector (eg, 1) TOTAL_FATS: 0x010, // 1 byte: FAT copies (eg, 2) ROOT_DIRENTS: 0x011, // 2 bytes: root directory entries (eg, 0x40 or 64) 0x40 * 0x20 = 0x800 (1 sector is 0x200 bytes, total of 4 sectors) TOTAL_SECS: 0x013, // 2 bytes: number of sectors (eg, 0x140 or 320); if zero, refer to LARGE_SECS MEDIA_TYPE: 0x015, // 1 byte: media type (see DiskAPI.FAT.MEDIA_*) FAT_SECS: 0x016, // 2 bytes: sectors per FAT (eg, 1) TRACK_SECS: 0x018, // 2 bytes: sectors per track (eg, 8) TOTAL_HEADS: 0x01A, // 2 bytes: number of heads (eg, 1) HIDDEN_SECS: 0x01C, // 2 bytes (DOS 2.x) or 4 bytes (DOS 3.31 and up): number of hidden sectors (always 0 for non-partitioned media) LARGE_SECS: 0x020 // 4 bytes (DOS 3.31 and up): number of sectors if TOTAL_SECS is zero }; /* * Common (supported) diskette geometries. * * Each entry in GEOMETRIES is an array of values in "CHS" order: * * [# cylinders, # heads, # sectors/track, # bytes/sector, media type] * * If the 4th value is omitted, the sector size is assumed to be 512. The order of these "geometric" values mirrors * the structure of our JSON-encoded disk images, which consist of an array of cylinders, each of which is an array of * heads, each of which is an array of sector objects. */ DiskAPI.GEOMETRIES = { 163840: [40,1,8,,0xFE], // media type 0xFE: 40 cylinders, 1 head (single-sided), 8 sectors/track, ( 320 total sectors x 512 bytes/sector == 163840) 184320: [40,1,9,,0xFC], // media type 0xFC: 40 cylinders, 1 head (single-sided), 9 sectors/track, ( 360 total sectors x 512 bytes/sector == 184320) 327680: [40,2,8,,0xFF], // media type 0xFF: 40 cylinders, 2 heads (double-sided), 8 sectors/track, ( 640 total sectors x 512 bytes/sector == 327680) 368640: [40,2,9,,0xFD], // media type 0xFD: 40 cylinders, 2 heads (double-sided), 9 sectors/track, ( 720 total sectors x 512 bytes/sector == 368640) 737280: [80,2,9,,0xF9], // media type 0xF9: 80 cylinders, 2 heads (double-sided), 9 sectors/track, (1440 total sectors x 512 bytes/sector == 737280) 1228800: [80,2,15,,0xF9], // media type 0xF9: 80 cylinders, 2 heads (double-sided), 15 sectors/track, (2400 total sectors x 512 bytes/sector == 1228800) 1474560: [80,2,18,,0xF0], // media type 0xF0: 80 cylinders, 2 heads (double-sided), 18 sectors/track, (2880 total sectors x 512 bytes/sector == 1474560) 2949120: [80,2,36,,0xF0], // media type 0xF0: 80 cylinders, 2 heads (double-sided), 36 sectors/track, (5760 total sectors x 512 bytes/sector == 2949120) /* * The following are some common disk sizes and their CHS values, since missing or bogus MBR and/or BPB values * might mislead us when attempting to determine the exact disk geometry. */ 10653696:[306,4,17], // PC XT 10Mb hard drive (type 3) 21411840:[615,4,17], // PC AT 20Mb hard drive (type 2) /* * Assorted DEC disk formats. */ 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 2494464: [203,2,12,512], // RK03 single-platter disk cartridge: 203 tracks, 2 heads, 12 sectors/track, 512 bytes/sector, for a total of 2494464 bytes 5242880: [256,2,40,256], // RL01K single-platter disk cartridge: 256 tracks, 2 heads, 40 sectors/track, 256 bytes/sector, for a total of 5242880 bytes 10485760:[512,2,40,256] // RL02K single-platter disk cartridge: 512 tracks, 2 heads, 40 sectors/track, 256 bytes/sector, for a total of 10485760 bytes }; /* * Media descriptor bytes for DOS-compatible FAT-formatted disks (stored in the first byte of the FAT) */ DiskAPI.FAT = { MEDIA_160KB: 0xFE, // 5.25-inch, 1-sided, 8-sector, 40-track MEDIA_180KB: 0xFC, // 5.25-inch, 1-sided, 9-sector, 40-track MEDIA_320KB: 0xFF, // 5.25-inch, 2-sided, 8-sector, 40-track MEDIA_360KB: 0xFD, // 5.25-inch, 2-sided, 9-sector, 40-track MEDIA_720KB: 0xF9, // 3.5-inch, 2-sided, 9-sector, 80-track MEDIA_1200KB: 0xF9, // 3.5-inch, 2-sided, 15-sector, 80-track MEDIA_FIXED: 0xF8, // fixed disk (aka hard drive) MEDIA_1440KB: 0xF0, // 3.5-inch, 2-sided, 18-sector, 80-track MEDIA_2880KB: 0xF0 // 3.5-inch, 2-sided, 36-sector, 80-track }; /* * Cluster constants for 12-bit FATs (CLUSNUM_FREE, CLUSNUM_RES and CLUSNUM_MIN are the same for all FATs) */ DiskAPI.FAT12 = { MAX_CLUSTERS: 4084, CLUSNUM_FREE: 0, // this should NEVER appear in cluster chain (except at the start of an empty chain) CLUSNUM_RES: 1, // reserved; this should NEVER appear in cluster chain CLUSNUM_MIN: 2, // smallest valid cluster number CLUSNUM_MAX: 0xFF6, // largest valid cluster number CLUSNUM_BAD: 0xFF7, // bad cluster; this should NEVER appear in cluster chain CLUSNUM_EOC: 0xFF8 // end of chain (actually, anything from 0xFF8-0xFFF indicates EOC) }; /* * Cluster constants for 16-bit FATs (CLUSNUM_FREE, CLUSNUM_RES and CLUSNUM_MIN are the same for all FATs) */ DiskAPI.FAT16 = { MAX_CLUSTERS: 65524, CLUSNUM_FREE: 0, // this should NEVER appear in cluster chain (except at the start of an empty chain) CLUSNUM_RES: 1, // reserved; this should NEVER appear in cluster chain CLUSNUM_MIN: 2, // smallest valid cluster number CLUSNUM_MAX: 0xFFF6, // largest valid cluster number CLUSNUM_BAD: 0xFFF7, // bad cluster; this should NEVER appear in cluster chain CLUSNUM_EOC: 0xFFF8 // end of chain (actually, anything from 0xFFF8-0xFFFF indicates EOC) }; /* * Directory Entry offsets (and assorted constants) in FAT disk images * * NOTE: Versions of DOS prior to 2.0 use INVALID exclusively to mark available directory entries; any entry marked * UNUSED will actually be considered USED. In DOS 2.0 and up, UNUSED was added to indicate that all remaining entries * are unused, relieving it from having to initialize the rest of the sectors in the directory cluster(s). And in fact, * you WILL encounter garbage in subsequent directory sectors if you attempt to read past an UNUSED entry. */ DiskAPI.DIRENT = { NAME: 0x000, // 8 bytes EXT: 0x008, // 3 bytes ATTR: 0x00B, // 1 byte MODTIME: 0x016, // 2 bytes MODDATE: 0x018, // 2 bytes CLUSTER: 0x01A, // 2 bytes SIZE: 0x01C, // 4 bytes (typically zero for subdirectories) LENGTH: 0x20, // 32 bytes total UNUSED: 0x00, // indicates this and all subsequent directory entries are unused INVALID: 0xE5 // indicates this directory entry is unused }; /* * Possible values for DIRENT.ATTR */ DiskAPI.ATTR = { READONLY: 0x01, // PC-DOS 2.0 and up HIDDEN: 0x02, SYSTEM: 0x04, LABEL: 0x08, // PC-DOS 2.0 and up SUBDIR: 0x10, // PC-DOS 2.0 and up ARCHIVE: 0x20 // PC-DOS 2.0 and up }; /** * @copyright http://pcjs.org/modules/shared/lib/dumpapi.js (C) Jeff Parsons 2012-2017 */ /* * Our "DiskDump API", such as it was, used to look like: * * http://jsmachines.net/bin/convdisk.php?disk=/disks/pc/dos/ibm/2.00/PCDOS200-DISK1.json&format=img * * To make it (a bit) more "REST-like", the above request now looks like: * * http://www.pcjs.org/api/v1/dump?disk=/disks/pc/dos/ibm/2.00/PCDOS200-DISK1.json&format=img * * Similarly, our "FileDump API" used to look like: * * http://jsmachines.net/bin/convrom.php?rom=/devices/pc/rom/5150/1981-04-24/PCBIOS-REV1.rom&format=json * * and that request now looks like: * * http://www.pcjs.org/api/v1/dump?file=/devices/pc/rom/5150/1981-04-24/PCBIOS-REV1.rom&format=json * * I don't think it makes sense to avoid "query" parameters, because blending the path of a disk image with the * the rest of the URL would be (a) confusing, and (b) more work to parse. */ var DumpAPI = { ENDPOINT: "/api/v1/dump", QUERY: { DIR: "dir", // value is path of a directory (DiskDump only) DISK: "disk", // value is path of a disk image (DiskDump only) FILE: "file", // value is path of a ROM image file (FileDump only) IMG: "img", // alias for DISK PATH: "path", // value is path of a one or more files (DiskDump only) FORMAT: "format", // value is one of FORMAT values below COMMENTS: "comments", // value is either "true" or "false" DECIMAL: "decimal", // value is either "true" to force all numbers to decimal, "false" or undefined otherwise MBHD: "mbhd", // value is hard drive size in Mb (formerly "mbsize") (DiskDump only) (DEPRECATED) SIZE: "size" // value is target disk size in Kb (supersedes "mbhd") (DiskDump only) }, FORMAT: { JSON: "json", // default JSON_GZ: "gz", // gzip is currently used ONLY for compressed JSON DATA: "data", // same as "json", but built without JSON.stringify() (DiskDump only) HEX: "hex", // deprecated OCTAL: "octal", // displays data as octal words BYTES: "bytes", // displays data as hex bytes; normally used only when comments are enabled WORDS: "words", // displays data as hex words; normally used only when comments are enabled LONGS: "longs", // displays data as dwords IMG: "img", // returns the raw disk data (ie, using a Buffer object) (DiskDump only) ROM: "rom" // returns the raw file data (ie, using a Buffer object) (FileDump only) } }; /* * Because we use an overloaded API endpoint (ie, one that's shared with the FileDump module), we must * also provide a list of commands which, when combined with the endpoint, define a unique request. */ DumpAPI.asDiskCommands = [DumpAPI.QUERY.DIR, DumpAPI.QUERY.DISK, DumpAPI.QUERY.PATH]; DumpAPI.asFileCommands = [DumpAPI.QUERY.FILE]; /** * @copyright http://pcjs.org/modules/shared/lib/reportapi.js (C) Jeff Parsons 2012-2017 */ var ReportAPI = { ENDPOINT: "/api/v1/report", QUERY: { APP: "app", VER: "ver", URL: "url", USER: "user", TYPE: "type", DATA: "data" }, TYPE: { BUG: "bug" }, RES: { OK: "Thank you" } }; /** * @copyright http://pcjs.org/modules/shared/lib/userapi.js (C) Jeff Parsons 2012-2017 */ /* * Examples of User API requests: * * web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.VERIFY + '&' + UserAPI.QUERY.USER + '=' + sUser; */ var UserAPI = { ENDPOINT: "/api/v1/user", QUERY: { REQ: "req", // specifies a request USER: "user", // specifies a user ID STATE: "state", // specifies a state ID DATA: "data" // specifies state data }, REQ: { CREATE: "create", // creates a user ID VERIFY: "verify", // requests verification of a user ID STORE: "store", // stores a machine state on the server LOAD: "load" // loads a machine state from the server }, RES: { CODE: "code", DATA: "data" }, CODE: { OK: "ok", FAIL: "error" }, FAIL: { DUPLICATE: "user already exists", VERIFY: "unable to verify user", BADSTATE: "invalid state parameter", NOSTATE: "no machine state", BADLOAD: "unable to load machine state", BADSTORE: "unable to save machine state" } }; /** * @copyright http://pcjs.org/modules/shared/lib/keys.js (C) Jeff Parsons 2012-2017 */ var Keys = { /* * Keys and/or key combinations that generate common ASCII codes. * * NOTE: If you're looking for a general-purpose ASCII code table, see Str.ASCII in strlib.js; * if something's missing, that's probably the more appropriate table to add it to. * * TODO: The Closure Compiler doesn't inline all references to these values, at least those with * quoted property names, which is why I've 'unquoted' as many of them as possible. One solution * would be to add mnemonics for all of them, not just the non-printable ones (eg, SPACE instead * of ' ', AMP instead of '&', etc.) */ ASCII: { BREAK: 0, CTRL_A: 1, CTRL_B: 2, CTRL_C: 3, CTRL_D: 4, CTRL_E: 5, CTRL_F: 6, CTRL_G: 7, CTRL_H: 8, CTRL_I: 9, CTRL_J: 10, CTRL_K: 11, CTRL_L: 12, CTRL_M: 13, CTRL_N: 14, CTRL_O: 15, CTRL_P: 16, CTRL_Q: 17, CTRL_R: 18, CTRL_S: 19, CTRL_T: 20, CTRL_U: 21, CTRL_V: 22, CTRL_W: 23, CTRL_X: 24, CTRL_Y: 25, CTRL_Z: 26, ' ': 32, '!': 33, '"': 34, '#': 35, '$': 36, '%': 37, '&': 38, "'": 39, '(': 40, ')': 41, '*': 42, '+': 43, ',': 44, '-': 45, '.': 46, '/': 47, '0': 48, '1': 49, '2': 50, '3': 51, '4': 52, '5': 53, '6': 54, '7': 55, '8': 56, '9': 57, ':': 58, ';': 59, '<': 60, '=': 61, '>': 62, '?': 63, '@': 64, A: 65, B: 66, C: 67, D: 68, E: 69, F: 70, G: 71, H: 72, I: 73, J: 74, K: 75, L: 76, M: 77, N: 78, O: 79, P: 80, Q: 81, R: 82, S: 83, T: 84, U: 85, V: 86, W: 87, X: 88, Y: 89, Z: 90, '[': 91, '\\':92, ']': 93, '^': 94, '_': 95, '`': 96, a: 97, b: 98, c: 99, d: 100, e: 101, f: 102, g: 103, h: 104, i: 105, j: 106, k: 107, l: 108, m: 109, n: 110, o: 111, p: 112, q: 113, r: 114, s: 115, t: 116, u: 117, v: 118, w: 119, x: 120, y: 121, z: 122, '{':123, '|':124, '}':125, '~':126, DEL: 127 }, /* * Browser keyCodes we must pay particular attention to. For the most part, these are non-alphanumeric * or function keys, some which may require special treatment (eg, preventDefault() if returning false on * the initial keyDown event is insufficient). * * keyCodes for most common ASCII keys can simply use the appropriate ASCII code above. * * Most of these represent non-ASCII keys (eg, the LEFT arrow key), yet for some reason, browsers defined * them using ASCII codes (eg, the LEFT arrow key uses the ASCII code for '%' or 37). */ KEYCODE: { /* 0x08 */ BS: 8, // BACKSPACE (ASCII.CTRL_H) /* 0x09 */ TAB: 9, // TAB (ASCII.CTRL_I) /* 0x0A */ LF: 10, // LINE FEED (ASCII.CTRL_J) (TODO: Determine if any key actually generates this) /* 0x0D */ CR: 13, // CARRIAGE RETURN (ASCII.CTRL_M) /* 0x10 */ SHIFT: 16, /* 0x11 */ CTRL: 17, /* 0x12 */ ALT: 18, /* 0x13 */ PAUSE: 19, // PAUSE/BREAK /* 0x14 */ CAPS_LOCK: 20, /* 0x1B */ ESC: 27, /* 0x20 */ SPACE: 32, /* 0x21 */ PGUP: 33, /* 0x22 */ PGDN: 34, /* 0x23 */ END: 35, /* 0x24 */ HOME: 36, /* 0x25 */ LEFT: 37, /* 0x26 */ UP: 38, /* 0x27 */ RIGHT: 39, /* 0x27 */ FF_QUOTE: 39, /* 0x28 */ DOWN: 40, /* 0x2C */ FF_COMMA: 44, /* 0x2C */ PRTSC: 44, /* 0x2D */ INS: 45, /* 0x2E */ DEL: 46, /* 0x2E */ FF_PERIOD: 46, /* 0x2F */ FF_SLASH: 47, /* 0x30 */ ZERO: 48, /* 0x31 */ ONE: 49, /* 0x32 */ TWO: 50, /* 0x33 */ THREE: 51, /* 0x34 */ FOUR: 52, /* 0x35 */ FIVE: 53, /* 0x36 */ SIX: 54, /* 0x37 */ SEVEN: 55, /* 0x38 */ EIGHT: 56, /* 0x39 */ NINE: 57, /* 0x3B */ FF_SEMI: 59, /* 0x3D */ FF_EQUALS: 61, /* 0x5B */ CMD: 91, // aka WIN /* 0x5B */ FF_LBRACK: 91, /* 0x5C */ FF_BSLASH: 92, /* 0x5D */ RCMD: 93, // aka MENU /* 0x5D */ FF_RBRACK: 93, /* 0x60 */ NUM_0: 96, /* 0x60 */ NUM_INS: 96, /* 0x60 */ FF_BQUOTE: 96, /* 0x61 */ NUM_1: 97, /* 0x61 */ NUM_END: 97, /* 0x62 */ NUM_2: 98, /* 0x62 */ NUM_DOWN: 98, /* 0x63 */ NUM_3: 99, /* 0x63 */ NUM_PGDN: 99, /* 0x64 */ NUM_4: 100, /* 0x64 */ NUM_LEFT: 100, /* 0x65 */ NUM_5: 101, /* 0x65 */ NUM_CENTER: 101, /* 0x66 */ NUM_6: 102, /* 0x66 */ NUM_RIGHT: 102, /* 0x67 */ NUM_7: 103, /* 0x67 */ NUM_HOME: 103, /* 0x68 */ NUM_8: 104, /* 0x68 */ NUM_UP: 104, /* 0x69 */ NUM_9: 105, /* 0x69 */ NUM_PGUP: 105, /* 0x6A */ NUM_MUL: 106, /* 0x6B */ NUM_ADD: 107, /* 0x6D */ NUM_SUB: 109, /* 0x6E */ NUM_DEL: 110, // aka PERIOD /* 0x6F */ NUM_DIV: 111, /* 0x70 */ F1: 112, /* 0x71 */ F2: 113, /* 0x72 */ F3: 114, /* 0x73 */ F4: 115, /* 0x74 */ F5: 116, /* 0x75 */ F6: 117, /* 0x76 */ F7: 118, /* 0x77 */ F8: 119, /* 0x78 */ F9: 120, /* 0x79 */ F10: 121, /* 0x7A */ F11: 122, /* 0x7B */ F12: 123, /* 0x90 */ NUM_LOCK: 144, /* 0x91 */ SCROLL_LOCK: 145, /* 0xAD */ FF_DASH: 173, /* 0xBA */ SEMI: 186, // Firefox: 59 (FF_SEMI) /* 0xBB */ EQUALS: 187, // Firefox: 61 (FF_EQUALS) /* 0xBC */ COMMA: 188, /* 0xBD */ DASH: 189, // Firefox: 173 (FF_DASH) /* 0xBE */ PERIOD: 190, /* 0xBF */ SLASH: 191, /* 0xC0 */ BQUOTE: 192, /* 0xDB */ LBRACK: 219, /* 0xDC */ BSLASH: 220, /* 0xDD */ RBRACK: 221, /* 0xDE */ QUOTE: 222, /* 0xE0 */ FF_CMD: 224, // Firefox only (used for both CMD and RCMD) // // The following biases use what I'll call Decimal Coded Binary or DCB (the opposite of BCD), // where the thousands digit is used to store the sum of "binary" digits 1 and/or 2 and/or 4. // // Technically, that makes it DCO (Decimal Coded Octal), but then again, BCD should have really // been called HCD (Hexadecimal Coded Decimal), so if "they" can take liberties, so can I. // // ONDOWN is a bias we add to browser keyCodes that we want to handle on "down" rather than on "press". // ONDOWN: 1000, // // ONRIGHT is a bias we add to browser keyCodes that need to check for a "right" location (default is "left") // ONRIGHT: 2000, // // FAKE is a bias we add to signal these are fake keyCodes corresponding to internal keystroke combinations. // The actual values are for internal use only and merely need to be unique and used consistently. // FAKE: 4000 }, /* * The set of values that a browser may store in the 'location' property of a keyboard event object * which we also support. */ LOCATION: { LEFT: 1, RIGHT: 2, NUMPAD: 3 } }; /* * Check the event object's 'location' property for a non-zero value for the following ONRIGHT keys. */ Keys.KEYCODE.NUM_CR = Keys.KEYCODE.CR + Keys.KEYCODE.ONRIGHT; /* * Maps Firefox keyCodes to their more common keyCode counterparts; a number of entries in this table * are no longer valid (if indeed they ever were), so they've been commented out. It's likely that I * simply extended this table to resolve additional differences in other browsers (ie, Opera), but without * browser-specific checks, it's not safe to perform all the mappings shown below. */ Keys.FF_KEYCODES = {}; Keys.FF_KEYCODES[Keys.KEYCODE.FF_SEMI] = Keys.KEYCODE.SEMI; // 59 -> 186 Keys.FF_KEYCODES[Keys.KEYCODE.FF_EQUALS] = Keys.KEYCODE.EQUALS; // 61 -> 187 Keys.FF_KEYCODES[Keys.KEYCODE.FF_DASH] = Keys.KEYCODE.DASH; // 173 -> 189 Keys.FF_KEYCODES[Keys.KEYCODE.FF_CMD] = Keys.KEYCODE.CMD; // 224 -> 91 // Keys.FF_KEYCODES[Keys.KEYCODE.FF_COMMA] = Keys.KEYCODE.COMMA; // 44 -> 188 // Keys.FF_KEYCODES[Keys.KEYCODE.FF_PERIOD] = Keys.KEYCODE.PERIOD; // 46 -> 190 // Keys.FF_KEYCODES[Keys.KEYCODE.FF_SLASH] = Keys.KEYCODE.SLASH; // 47 -> 191 // Keys.FF_KEYCODES[Keys.KEYCODE.FF_BQUOTE] = Keys.KEYCODE.BQUOTE; // 96 -> 192 // Keys.FF_KEYCODES[Keys.KEYCODE.FF_LBRACK = Keys.KEYCODE.LBRACK; // 91 -> 219 // Keys.FF_KEYCODES[Keys.KEYCODE.FF_BSLASH] = Keys.KEYCODE.BSLASH; // 92 -> 220 // Keys.FF_KEYCODES[Keys.KEYCODE.FF_RBRACK] = Keys.KEYCODE.RBRACK; // 93 -> 221 // Keys.FF_KEYCODES[Keys.KEYCODE.FF_QUOTE] = Keys.KEYCODE.QUOTE; // 39 -> 222 /* * Maps non-ASCII keyCodes to their ASCII counterparts */ Keys.NONASCII_KEYCODES = {}; Keys.NONASCII_KEYCODES[Keys.KEYCODE.FF_DASH] = Keys.ASCII['-']; // 173 -> 45 Keys.NONASCII_KEYCODES[Keys.KEYCODE.SEMI] = Keys.ASCII[';']; // 186 -> 59 Keys.NONASCII_KEYCODES[Keys.KEYCODE.EQUALS] = Keys.ASCII['=']; // 187 -> 61 Keys.NONASCII_KEYCODES[Keys.KEYCODE.DASH] = Keys.ASCII['-']; // 189 -> 45 Keys.NONASCII_KEYCODES[Keys.KEYCODE.COMMA] = Keys.ASCII[',']; // 188 -> 44 Keys.NONASCII_KEYCODES[Keys.KEYCODE.PERIOD] = Keys.ASCII['.']; // 190 -> 46 Keys.NONASCII_KEYCODES[Keys.KEYCODE.SLASH] = Keys.ASCII['/']; // 191 -> 47 Keys.NONASCII_KEYCODES[Keys.KEYCODE.BQUOTE] = Keys.ASCII['`']; // 192 -> 96 Keys.NONASCII_KEYCODES[Keys.KEYCODE.LBRACK] = Keys.ASCII['[']; // 219 -> 91 Keys.NONASCII_KEYCODES[Keys.KEYCODE.BSLASH] = Keys.ASCII['\\']; // 220 -> 92 Keys.NONASCII_KEYCODES[Keys.KEYCODE.RBRACK] = Keys.ASCII[']']; // 221 -> 93 Keys.NONASCII_KEYCODES[Keys.KEYCODE.QUOTE] = Keys.ASCII["'"]; // 222 -> 39 /* * Maps unshifted keyCodes to their shifted counterparts; to be used when a shift-key is down. * Alphabetic characters are handled in code, since they must also take CAPS_LOCK into consideration. */ Keys.SHIFTED_KEYCODES = {}; Keys.SHIFTED_KEYCODES[Keys.ASCII['1']] = Keys.ASCII['!']; Keys.SHIFTED_KEYCODES[Keys.ASCII['2']] = Keys.ASCII['@']; Keys.SHIFTED_KEYCODES[Keys.ASCII['3']] = Keys.ASCII['#']; Keys.SHIFTED_KEYCODES[Keys.ASCII['4']] = Keys.ASCII['$']; Keys.SHIFTED_KEYCODES[Keys.ASCII['5']] = Keys.ASCII['%']; Keys.SHIFTED_KEYCODES[Keys.ASCII['6']] = Keys.ASCII['^']; Keys.SHIFTED_KEYCODES[Keys.ASCII['7']] = Keys.ASCII['&']; Keys.SHIFTED_KEYCODES[Keys.ASCII['8']] = Keys.ASCII['*']; Keys.SHIFTED_KEYCODES[Keys.ASCII['9']] = Keys.ASCII['(']; Keys.SHIFTED_KEYCODES[Keys.ASCII['0']] = Keys.ASCII[')']; Keys.SHIFTED_KEYCODES[Keys.KEYCODE.SEMI] = Keys.ASCII[':']; Keys.SHIFTED_KEYCODES[Keys.KEYCODE.EQUALS] = Keys.ASCII['+']; Keys.SHIFTED_KEYCODES[Keys.KEYCODE.COMMA] = Keys.ASCII['<']; Keys.SHIFTED_KEYCODES[Keys.KEYCODE.DASH] = Keys.ASCII['_']; Keys.SHIFTED_KEYCODES[Keys.KEYCODE.PERIOD] = Keys.ASCII['>']; Keys.SHIFTED_KEYCODES[Keys.KEYCODE.SLASH] = Keys.ASCII['?']; Keys.SHIFTED_KEYCODES[Keys.KEYCODE.BQUOTE] = Keys.ASCII['~']; Keys.SHIFTED_KEYCODES[Keys.KEYCODE.LBRACK] = Keys.ASCII['{']; Keys.SHIFTED_KEYCODES[Keys.KEYCODE.BSLASH] = Keys.ASCII['|']; Keys.SHIFTED_KEYCODES[Keys.KEYCODE.RBRACK] = Keys.ASCII['}']; Keys.SHIFTED_KEYCODES[Keys.KEYCODE.QUOTE] = Keys.ASCII['"']; Keys.SHIFTED_KEYCODES[Keys.KEYCODE.FF_DASH] = Keys.ASCII['_']; Keys.SHIFTED_KEYCODES[Keys.KEYCODE.FF_EQUALS] = Keys.ASCII['+']; Keys.SHIFTED_KEYCODES[Keys.KEYCODE.FF_SEMI] = Keys.ASCII[':']; /** * @copyright http://pcjs.org/modules/shared/lib/strlib.js (C) Jeff Parsons 2012-2017 */ class Str { /** * isValidInt(s, base) * * The built-in parseInt() function has the annoying feature of returning a partial value (ie, * up to the point where it encounters an invalid character); eg, parseInt("foo", 16) returns 0xf. * * So it's best to use our own Str.parseInt() function, which will in turn use this function to * validate the entire string. * * @param {string} s is the string representation of some number * @param {number} [base] is the radix to use (default is 10); only 2, 8, 10 and 16 are supported * @return {boolean} true if valid, false if invalid (or the specified base isn't supported) */ static isValidInt(s, base) { if (!base || base == 10) return s.match(/^-?[0-9]+$/) !== null; if (base == 16) return s.match(/^-?[0-9a-f]+$/i) !== null; if (base == 8) return s.match(/^-?[0-7]+$/) !== null; if (base == 2) return s.match(/^-?[01]+$/) !== null; return false; } /** * parseInt(s, base) * * This is a wrapper around the built-in parseInt() function. Our wrapper recognizes certain prefixes * ('$' or "0x" for hex, '#' or "0o" for octal) and suffixes ('.' for decimal, 'h' for hex, 'y' for * binary), and then calls isValidInt() to ensure we don't convert strings that contain partial values; * see isValidInt() for details. * * The use of multiple prefix/suffix combinations is undefined (although for the record, we process * prefixes first). We do NOT support the "0b" prefix to indicate binary UNLESS one or more commas are * also present (because "0b" is also a valid hex sequence), and we do NOT support a single leading zero * to indicate octal (because such a number could also be decimal or hex). Any number of commas are * allowed; we remove them all before calling the built-in parseInt(). * * More recently, we've added support for "^D", "^O", and "^B" prefixes to accommodate the base overrides * that the PDP-10's MACRO-10 assembly language supports (decimal, octal, and binary, respectively). * If this support turns out to adversely affect other debuggers, then it will have to be "conditionalized". * Similarly, we've added support for "K", "M", and "G" MACRO-10-style suffixes that add 3, 6, or 9 zeros * to the value to be parsed, respectively. * * @param {string} s is the string representation of some number * @param {number} [base] is the radix to use (default is 10); can be overridden by prefixes/suffixes * @return {number|undefined} corresponding value, or undefined if invalid */ static parseInt(s, base) { var value; if (s) { if (!base) base = 10; var ch, chPrefix, chSuffix; var fCommas = (s.indexOf(',') > 0); if (fCommas) s = s.replace(/,/g, ''); ch = chPrefix = s.charAt(0); if (chPrefix == '#') { base = 8; chPrefix = ''; } else if (chPrefix == '$') { base = 16; chPrefix = ''; } if (ch != chPrefix) { s = s.substr(1); } else { ch = chPrefix = s.substr(0, 2); if (chPrefix == '0b' && fCommas || chPrefix == '^B') { base = 2; chPrefix = ''; } else if (chPrefix == '0o' || chPrefix == '^O') { base = 8; chPrefix = ''; } else if (chPrefix == '^D') { base = 10; chPrefix = ''; } else if (chPrefix == '0x') { base = 16; chPrefix = ''; } if (ch != chPrefix) s = s.substr(2); } ch = chSuffix = s.slice(-1); if (chSuffix == 'Y' || chSuffix == 'y') { base = 2; chSuffix = ''; } else if (chSuffix == '.') { base = 10; chSuffix = ''; } else if (chSuffix == 'H' || chSuffix == 'h') { base = 16; chSuffix = ''; } else if (chSuffix == 'K') { chSuffix = '000'; } else if (chSuffix == 'M') { chSuffix = '000000'; } else if (chSuffix == 'G') { chSuffix = '000000000'; } if (ch != chSuffix) s = s.slice(0, -1) + chSuffix; /* * This adds support for the MACRO-10 binary shifting (Bn) suffix, which must be stripped from the * number before parsing, and then applied to the value after parsing. If n is omitted, 35 is assumed, * which is a net shift of zero. If n < 35, then a left shift of (35 - n) is required; if n > 35, then * a right shift of -(35 - n) is required. */ var v, shift = 0; if (base <= 10) { var match = s.match(/(-?[0-9]+)B([0-9]*)/); if (match) { s = match[1]; shift = 35 - ((match[2] || 35) & 0xff); } } if (Str.isValidInt(s, base) && !isNaN(v = parseInt(s, base))) { /* * With the need to support larger (eg, 36-bit) integers, truncating to 32 bits is no longer helpful. * * value = v|0; */ if (shift) { /* * Since binary shifting is a logical operation, and since shifting by division only works properly * with positive numbers, we must convert a negative value to a positive value, by computing the two's * complement. */ if (v < 0) v += Math.pow(2, 36); if (shift > 0) { v *= Math.pow(2, shift); } else { v = Math.trunc(v / Math.pow(2, -shift)); } } value = v; } } return value; } /** * toBase(n, radix, cch, sPrefix, nGrouping) * * Displays the given number as an unsigned integer using the specified radix and number of digits. * * @param {number|null|undefined} n * @param {number} radix (ie, the base) * @param {number} cch (the desired number of digits) * @param {string} [sPrefix] (default is none) * @param {number} [nGrouping] * @return {string} */ static toBase(n, radix, cch, sPrefix = "", nGrouping = 0) { /* * An initial "falsey" check for null takes care of both null and undefined; * we can't rely entirely on isNaN(), because isNaN(null) returns false, oddly enough. * * Alternatively, we could mask and shift n regardless of whether it's null/undefined/NaN, * since JavaScript coerces such operands to zero, but I think there's "value" in seeing those * values displayed differently. */ var s = ""; if (isNaN(n)) { n = null; } else if (n != null) { /* * Callers that produced an input by dividing by a power of two rather than shifting (in order * to access more than 32 bits) may produce a fractional result, which ordinarily we would simply * ignore, but if the integer portion is zero and the sign is negative, we should probably treat * this value as a sign-extension. */ if (n < 0 && n > -1) n = -1; /* * Negative values should be two's complemented according to the number of digits; for example, * 12 octal digits implies an upper limit 8^12. */ if (n < 0) { n += Math.pow(radix, cch); } if (n >= Math.pow(radix, cch)) { cch = Math.ceil(Math.log(n) / Math.log(radix)); } } var g = nGrouping || -1; while (cch-- > 0) { if (!g) { s = ',' + s; g = nGrouping; } if (n == null) { s = '?' + s; } else { var d = n % radix; d += (d >= 0 && d <= 9? 0x30 : 0x41 - 10); s = String.fromCharCode(d) + s; n = Math.trunc(n / radix); } g--; } return sPrefix + s; } /** * toBin(n, cch, nGrouping) * * Converts an integer to binary, with the specified number of digits (up to a maximum of 36). * * @param {number|null|undefined} n (supports integers up to 36 bits now) * @param {number} [cch] is the desired number of binary digits (0 or undefined for default of either 8, 18, or 36) * @param {number} [nGrouping] * @return {string} the binary representation of n */ static toBin(n, cch, nGrouping) { if (!cch) { // cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.LN2) || 1; var v = Math.abs(n); if (v <= 0b11111111) { cch = 8; } else if (v <= 0b111111111111111111) { cch = 18; } else { cch = 36; } } else if (cch > 36) cch = 36; return Str.toBase(n, 2, cch, "", nGrouping); } /** * toBinBytes(n, cb, fPrefix) * * Converts an integer to binary, with the specified number of bytes (up to the default of 4). * * @param {number|null|undefined} n (interpreted as a 32-bit value) * @param {number} [cb] is the desired number of binary bytes (4 is both the default and the maximum) * @param {boolean} [fPrefix] * @return {string} the binary representation of n */ static toBinBytes(n, cb, fPrefix) { var s = ""; if (!cb || cb > 4) cb = 4; for (var i = 0; i < cb; i++) { if (s) s = ',' + s; s = Str.toBin(n & 0xff, 8) + s; n >>= 8; } return (fPrefix? "0b" : "") + s; } /** * toOct(n, cch, fPrefix) * * Converts an integer to octal, with the specified number of digits (default of 6; max of 12) * * You might be tempted to use the built-in n.toString(8) instead, but it doesn't zero-pad and it * doesn't properly convert negative values. Moreover, if n is undefined, n.toString() will throw * an exception, whereas this function will return '?' characters. * * @param {number|null|undefined} n (supports integers up to 36 bits now) * @param {number} [cch] is the desired number of octal digits (0 or undefined for default of either 6, 8, or 12) * @param {boolean} [fPrefix] * @return {string} the octal representation of n */ static toOct(n, cch, fPrefix) { if (!cch) { // cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.log(8)) || 1; var v = Math.abs(n); if (v <= 0o777777) { cch = 6; } else if (v <= 0o77777777) { cch = 8; } else { cch = 12; } } else if (cch > 12) cch = 12; return Str.toBase(n, 8, cch, fPrefix? "0o" : ""); } /** * toDec(n, cch) * * Converts an integer to decimal, with the specified number of digits (default of 5; max of 11) * * You might be tempted to use the built-in n.toString(10) instead, but it doesn't zero-pad and it * doesn't properly convert negative values. Moreover, if n is undefined, n.toString() will throw * an exception, whereas this function will return '?' characters. * * @param {number|null|undefined} n (supports integers up to 36 bits now) * @param {number} [cch] is the desired number of decimal digits (0 or undefined for default of either 5 or 11) * @return {string} the decimal representation of n */ static toDec(n, cch) { if (!cch) { // cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.LN10) || 1; var v = Math.abs(n); if (v <= 99999) { cch = 5; } else { cch = 11; } } else if (cch > 11) cch = 11; return Str.toBase(n, 10, cch); } /** * toHex(n, cch, fPrefix) * * Converts an integer to hex, with the specified number of digits (default of 4 or 8, max of 9). * * You might be tempted to use the built-in n.toString(16) instead, but it doesn't zero-pad and it * doesn't properly convert negative values; for example, if n is -2147483647, then n.toString(16) * will return "-7fffffff" instead of "80000001". Moreover, if n is undefined, n.toString() will * throw an exception, whereas this function will return '?' characters. * * NOTE: The following work-around (adapted from code found on StackOverflow) would be another solution, * taking care of negative values, zero-padding, and upper-casing, but not null/undefined/NaN values: * * s = (n < 0? n + 0x100000000 : n).toString(16); * s = "00000000".substr(0, 8 - s.length) + s; * s = s.substr(0, cch).toUpperCase(); * * @param {number|null|undefined} n (supports integers up to 36 bits now) * @param {number} [cch] is the desired number of hex digits (0 or undefined for default of either 4, 8, or 9) * @param {boolean} [fPrefix] * @return {string} the hex representation of n */ static toHex(n, cch, fPrefix) { if (!cch) { // cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.log(16)) || 1; var v = Math.abs(n); if (v <= 0xffff) { cch = 4; } else if (v <= 0xffffffff) { cch = 8; } else { cch = 9; } } else if (cch > 9) cch = 9; return Str.toBase(n, 16, cch, fPrefix? "0x" : ""); } /** * toHexByte(b) * * Alias for Str.toHex(b, 2, true) * * @param {number|null|undefined} b is a byte value * @return {string} the hex representation of b */ static toHexByte(b) { return Str.toHex(b, 2, true); } /** * toHexWord(w) * * Alias for Str.toHex(w, 4, true) * * @param {number|null|undefined} w is a word (16-bit) value * @return {string} the hex representation of w */ static toHexWord(w) { return Str.toHex(w, 4, true); } /** * toHexLong(l) * * Alias for Str.toHex(l, 8, true) * * @param {number|null|undefined} l is a dword (32-bit) value * @return {string} the hex representation of w */ static toHexLong(l) { return Str.toHex(l, 8, true); } /** * getBaseName(sFileName, fStripExt) * * This is a poor-man's version of Node's path.basename(), which Node-only components should use instead. * * Note that if fStripExt is true, this strips ANY extension, whereas path.basename() strips the extension only * if it matches the second parameter (eg, path.basename("/foo/bar/baz/asdf/quux.html", ".html") returns "quux"). * * @param {string} sFileName * @param {boolean} [fStripExt] * @return {string} */ static getBaseName(sFileName, fStripExt) { var sBaseName = sFileName; var i = sFileName.lastIndexOf('/'); if (i >= 0) sBaseName = sFileName.substr(i + 1); /* * This next bit is a kludge to clean up names that are part of a URL that includes unsightly query parameters. */ i = sBaseName.indexOf('&'); if (i > 0) sBaseName = sBaseName.substr(0, i); if (fStripExt) { i = sBaseName.lastIndexOf("."); if (i > 0) { sBaseName = sBaseName.substring(0, i); } } return sBaseName; } /** * getExtension(sFileName) * * This is a poor-man's version of Node's path.extname(), which Node-only components should use instead. * * Note that we EXCLUDE the period from the returned extension, whereas path.extname() includes it. * * @param {string} sFileName * @return {string} the filename's extension (in lower-case and EXCLUDING the "."), or an empty string */ static getExtension(sFileName) { var sExtension = ""; var i = sFileName.lastIndexOf("."); if (i >= 0) { sExtension = sFileName.substr(i + 1).toLowerCase(); } return sExtension; } /** * endsWith(s, sSuffix) * * @param {string} s * @param {string} sSuffix * @return {boolean} true if s ends with sSuffix, false if not */ static endsWith(s, sSuffix) { return s.indexOf(sSuffix, s.length - sSuffix.length) !== -1; } /** * escapeHTML(sHTML) * * @param {string} sHTML * @return {string} with HTML entities "escaped", similar to PHP's htmlspecialchars() */ static escapeHTML(sHTML) { return sHTML.replace(/[&<>"']/g, function(m) { return Str.aHTMLEscapeMap[m]; }); } /** * replace(sFind, sReplace, s) * * The JavaScript replace() function ALWAYS interprets "$" specially in replacement strings, even when * the search string is NOT a RegExp; specifically: * * $$ Inserts a "$" * $& Inserts the matched substring * $` Inserts the portion of the string that precedes the matched substring * $' Inserts the portion of the string that follows the matched substring * $n Where n is a positive integer less than 100, inserts the nth parenthesized sub-match string, * provided the first argument was a RegExp object * * So, if a replacement string containing dollar signs passes through a series of replace() calls, untold * problems could result. Hence, this function, which simply uses the replacement string as-is. * * Similar to the JavaScript replace() method, this replaces only one occurrence (ie, the FIRST occurrence); * it might be nice to add options to replace the LAST occurrence and/or ALL occurrences, but we'll revisit * that later. * * @param {string} sFind * @param {string} sReplace * @param {string} s * @return {string} */ static replace(sFind, sReplace, s) { var i = s.indexOf(sFind); if (i >= 0) { s = s.substr(0, i) + sReplace + s.substr(i + sFind.length); } return s; } /** * replaceAll(sFind, sReplace, s) * * @param {string} sFind * @param {string} sReplace * @param {string} s * @return {string} */ static replaceAll(sFind, sReplace, s) { var a = {}; a[sFind] = sReplace; return Str.replaceArray(a, s); } /** * replaceArray(a, s) * * @param {Object} a * @param {string} s * @return {string} */ static replaceArray(a, s) { var sMatch = ""; for (var k in a) { /* * As noted in: * * http://www.regexguru.com/2008/04/escape-characters-only-when-necessary/ * * inside character classes, only backslash, caret, hyphen and the closing bracket need to be * escaped. And in fact, if you ensure that the closing bracket is first, the caret is not first, * and the hyphen is last, you can avoid escaping those as well. */ k = k.replace(/([\\[\]*{}().+?])/g, "\\$1"); sMatch += (sMatch? '|' : '') + k; } return s.replace(new RegExp('(' + sMatch + ')', "g"), function(m) { return a[m]; }); } /** * pad(s, cch, fPadLeft) * * NOTE: the maximum amount of padding currently supported is 40 spaces. * * @param {string} s is a string * @param {number} cch is desired length * @param {boolean} [fPadLeft] (default is padding on the right) * @return {string} the original string (s) with spaces padding it to the specified length */ static pad(s, cch, fPadLeft) { var sPadding = " "; return fPadLeft? (sPadding + s).slice(-cch) : (s + sPadding).slice(0, cch); } /** * stripLeadingZeros(s, fPad) * * @param {string} s * @param {boolean} [fPad] * @return {string} */ static stripLeadingZeros(s, fPad) { var cch = s.length; s = s.replace(/^0+([0-9A-F]+)$/i, "$1"); if (fPad) s = Str.pad(s, cch, true); return s; } /** * trim(s) * * @param {string} s * @return {string} */ static trim(s) { if (String.prototype.trim) { return s.trim(); } return s.replace(/^\s+|\s+$/g, ""); } /** * toASCIICode(b) * * @param {number} b * @return {string} */ static toASCIICode(b) { var s; if (b != Str.ASCII.CR && b != Str.ASCII.LF) { s = Str.aASCIICodes[b]; } if (s) { s = '<' + s + '>'; } else { s = String.fromCharCode(b); } return s; } } Str.aHTMLEscapeMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; /* * Future home of a general-purpose ASCII table. TODO: Flesh it out. */ Str.ASCII = { LF: 0x0A, CR: 0x0D }; /* * Table for converting "unprintable" ASCII codes into mnemonics, to more clearly see what's being printed. */ Str.aASCIICodes = { 0x00: "NUL", 0x01: "SOH", // (CTRL_A) Start of Heading 0x02: "STX", // (CTRL_B) Start of Text 0x03: "ETX", // (CTRL_C) End of Text 0x04: "EOT", // (CTRL_D) End of Transmission 0x05: "ENQ", // (CTRL_E) Enquiry 0x06: "ACK", // (CTRL_F) Acknowledge 0x07: "BEL", // (CTRL_G) Bell 0x08: "BS", // (CTRL_H) Backspace 0x09: "TAB", // (CTRL_I) Horizontal Tab 0x0A: "LF", // (CTRL_J) Line Feed (New Line) 0x0B: "VT", // (CTRL_K) Vertical Tab 0x0C: "FF", // (CTRL_L) Form Feed (New Page) 0x0D: "CR", // (CTRL_M) Carriage Return 0x0E: "SO", // (CTRL_N) Shift Out 0x0F: "SI", // (CTRL_O) Shift In 0x10: "DLE", // (CTRL_P) Data Link Escape 0x11: "XON", // (CTRL_Q) Device Control 1 (aka DC1) 0x12: "DC2", // (CTRL_R) Device Control 2 0x13: "XOFF", // (CTRL_S) Device Control 3 (aka DC3) 0x14: "DC4", // (CTRL_T) Device Control 4 0x15: "NAK", // (CTRL_U) Negative Acknowledge 0x16: "SYN", // (CTRL_V) Synchronous Idle 0x17: "ETB", // (CTRL_W) End of Transmission Block 0x18: "CAN", // (CTRL_X) Cancel 0x19: "EM", // (CTRL_Y) End of Medium 0x1A: "SUB", // (CTRL_Z) Substitute 0x1B: "ESC", // Escape 0x1C: "FS", // File Separator 0x1D: "GS", // Group Separator 0x1E: "RS", // Record Separator 0x1F: "US" // Unit Separator }; Str.TYPES = { NULL: 0, BYTE: 1, WORD: 2, DWORD: 3, NUMBER: 4, STRING: 5, BOOLEAN: 6, OBJECT: 7, ARRAY: 8 }; /** * @copyright http://pcjs.org/modules/shared/lib/usrlib.js (C) Jeff Parsons 2012-2017 */ /** * @typedef {{ * mask: number, * shift: number * }} */ var BitField; /** * @typedef {Object.} */ var BitFields; class Usr { /** * binarySearch(a, v, fnCompare) * * @param {Array} a is an array * @param {number|string|Array|Object} v * @param {function((number|string|Array|Object), (number|string|Array|Object))} [fnCompare] * @return {number} the index of matching entry if non-negative, otherwise the index of the insertion point */ static binarySearch(a, v, fnCompare) { var left = 0; var right = a.length; var found = 0; if (fnCompare === undefined) { fnCompare = function(a, b) { return a > b ? 1 : a < b ? -1 : 0; }; } while (left < right) { var middle = (left + right) >> 1; var compareResult; compareResult = fnCompare(v, a[middle]); if (compareResult > 0) { left = middle + 1; } else { right = middle; found = !compareResult; } } return found ? left : ~left; } /** * binaryInsert(a, v, fnCompare) * * If element v already exists in array a, the array is unchanged (we don't allow duplicates); otherwise, the * element is inserted into the array at the appropriate index. * * @param {Array} a is an array * @param {number|string|Array|Object} v is the value to insert * @param {function((number|string|Array|Object), (number|string|Array|Object))} [fnCompare] */ static binaryInsert(a, v, fnCompare) { var index = Usr.binarySearch(a, v, fnCompare); if (index < 0) { a.splice(-(index + 1), 0, v); } } /** * getTimestamp() * * @return {string} timestamp containing the current date and time ("yyyy-mm-dd hh:mm:ss") */ static getTimestamp() { var date = new Date(); var padNum = function(n) { return (n < 10 ? "0" : "") + n; }; return date.getFullYear() + "-" + padNum(date.getMonth() + 1) + "-" + padNum(date.getDate()) + " " + padNum(date.getHours()) + ":" + padNum(date.getMinutes()) + ":" + padNum(date.getSeconds()); } /** * getMonthDays(nMonth, nYear) * * Note that if we're being called on behalf of the RTC, its year is always truncated to two digits (mod 100), * so we have no idea what century the year 0 might refer to. When using the normal leap-year formula, 0 fails * the mod 100 test but passes the mod 400 test, so as far as the RTC is concerned, every century year is a leap * year. Since we're most likely dealing with the year 2000, that's fine, since 2000 was also a leap year. * * TODO: There IS a separate CMOS byte that's supposed to be set to CMOS_ADDR.CENTURY_DATE; it's always BCD, * so theoretically it will contain values like 0x19 or 0x20 (for the 20th and 21st centuries, respectively), and * we could add that as another parameter to this function, to improve the accuracy, but that would go beyond what * a real RTC actually does. * * @param {number} nMonth (1-12) * @param {number} nYear (normally a 4-digit year, but it may also be mod 100) * @return {number} the maximum (1-based) day allowed for the specified month and year */ static getMonthDays(nMonth, nYear) { var nDays = Usr.aMonthDays[nMonth - 1]; if (nDays == 28) { if ((nYear % 4) === 0 && ((nYear % 100) || (nYear % 400) === 0)) { nDays++; } } return nDays; } /** * formatDate(sFormat, date) * * @param {string} sFormat (eg, "F j, Y", "Y-m-d H:i:s") * @param {Date} [date] (default is the current time) * @return {string} * * Supported identifiers in sFormat include: * * a: lowercase ante meridiem and post meridiem (am or pm) * d: day of the month, 2 digits with leading zeros (01,02,...,31) * D: 3-letter day of the week ("Sun","Mon",...,"Sat") * F: month ("January","February",...,"December") * g: hour in 12-hour format, without leading zeros (1,2,...,12) * h: hour in 24-hour format, without leading zeros (0,1,...,23) * H: hour in 24-hour format, with leading zeros (00,01,...,23) * i: minutes, with leading zeros (00,01,...,59) * j: day of the month, without leading zeros (1,2,...,31) * l: day of the week ("Sunday","Monday",...,"Saturday") * m: month, with leading zeros (01,02,...,12) * M: 3-letter month ("Jan","Feb",...,"Dec") * n: month, without leading zeros (1,2,...,12) * s: seconds, with leading zeros (00,01,...,59) * y: 2-digit year (eg, 14) * Y: 4-digit year (eg, 2014) * * For more inspiration, see: http://php.net/manual/en/function.date.php (of which we support ONLY a subset). */ static formatDate(sFormat, date) { var sDate = ""; if (!date) date = new Date(); var iHour = date.getHours(); var iDay = date.getDate(); var iMonth = date.getMonth() + 1; for (var i = 0; i < sFormat.length; i++) { var ch; switch ((ch = sFormat.charAt(i))) { case 'a': sDate += (iHour < 12 ? "am" : "pm"); break; case 'd': sDate += ('0' + iDay).slice(-2); break; case 'D': sDate += Usr.asDays[date.getDay()].substr(0, 3); break; case 'F': sDate += Usr.asMonths[iMonth - 1]; break; case 'g': sDate += (!iHour ? 12 : (iHour > 12 ? iHour - 12 : iHour)); break; case 'h': sDate += iHour; break; case 'H': sDate += ('0' + iHour).slice(-2); break; case 'i': sDate += ('0' + date.getMinutes()).slice(-2); break; case 'j': sDate += iDay; break; case 'l': sDate += Usr.asDays[date.getDay()]; break; case 'm': sDate += ('0' + iMonth).slice(-2); break; case 'M': sDate += Usr.asMonths[iMonth - 1].substr(0, 3); break; case 'n': sDate += iMonth; break; case 's': sDate += ('0' + date.getSeconds()).slice(-2); break; case 'y': sDate += ("" + date.getFullYear()).slice(-2); break; case 'Y': sDate += date.getFullYear(); break; default: sDate += ch; break; } } return sDate; } /** * defineBitFields(bfs) * * Prepares a bit field definition for use with getBitField() and setBitField(); eg: * * var bfs = Usr.defineBitFields({num:20, count:8, btmod:1, type:3}); * * The above defines a set of bit fields containing four fields: num (bits 0-19), count (bits 20-27), btmod (bit 28), and type (bits 29-31). * * Usr.setBitField(bfs.num, n, 1); * * The above set bit field "bfs.num" in numeric variable "n" to the value 1. * * @param {Object} bfs * @return {BitFields} */ static defineBitFields(bfs) { var bit = 0; for (var f in bfs) { var width = bfs[f]; var mask = ((1 << width) - 1) << bit; bfs[f] = {mask: mask, shift: bit}; bit += width; } return bfs; } /** * initBitFields(bfs, ...) * * @param {BitFields} bfs * @param {...number} var_args * @return {number} a value containing all supplied bit fields */ static initBitFields(bfs, var_args) { var v = 0, i = 1; for (var f in bfs) { if (i >= arguments.length) break; v = Usr.setBitField(bfs[f], v, arguments[i++]); } return v; } /** * getBitField(bf, v) * * @param {BitField} bf * @param {number} v is a value containing bit fields * @return {number} the value of the bit field in v defined by bf */ static getBitField(bf, v) { return (v & bf.mask) >> bf.shift; } /** * setBitField(bf, v, n) * * @param {BitField} bf * @param {number} v is a value containing bit fields * @param {number} n is a value to store in v in the bit field defined by bf * @return {number} updated v */ static setBitField(bf, v, n) { return (v & ~bf.mask) | ((n << bf.shift) & bf.mask); } /** * indexOf(a, t, i) * * Use this instead of Array.prototype.indexOf() if you can't be sure the browser supports it. * * @param {Array} a * @param {*} t * @param {number} [i] * @returns {number} */ static indexOf(a, t, i) { if (Array.prototype.indexOf) { return a.indexOf(t, i); } i = i || 0; if (i < 0) i += a.length; if (i < 0) i = 0; for (var n = a.length; i < n; i++) { if (i in a && a[i] === t) return i; } return -1; } } Usr.asDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; Usr.asMonths = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; Usr.aMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; /** * getTime() * * @return {number} the current time, in milliseconds */ Usr.getTime = Date.now || function() { return +new Date(); }; /** * @copyright http://pcjs.org/modules/shared/lib/weblib.js (C) Jeff Parsons 2012-2017 */ /* * According to http://www.w3schools.com/jsref/jsref_obj_global.asp, these are the *global* properties * and functions of JavaScript-in-the-Browser: * * Property Description * --- * Infinity A numeric value that represents positive/negative infinity * NaN "Not-a-Number" value * undefined Indicates that a variable has not been assigned a value * * Function Description * --- * decodeURI() Decodes a URI * decodeURIComponent() Decodes a URI component * encodeURI() Encodes a URI * encodeURIComponent() Encodes a URI component * escape() Deprecated in version 1.5. Use encodeURI() or encodeURIComponent() instead * eval() Evaluates a string and executes it as if it was script code * isFinite() Determines whether a value is a finite, legal number * isNaN() Determines whether a value is an illegal number * Number() Converts an object's value to a number * parseFloat() Parses a string and returns a floating point number * parseInt() Parses a string and returns an integer * String() Converts an object's value to a string * unescape() Deprecated in version 1.5. Use decodeURI() or decodeURIComponent() instead * * And according to http://www.w3schools.com/jsref/obj_window.asp, these are the properties and functions * of the *window* object. * * Property Description * --- * closed Returns a Boolean value indicating whether a window has been closed or not * defaultStatus Sets or returns the default text in the statusbar of a window * document Returns the Document object for the window (See Document object) * frames Returns an array of all the frames (including iframes) in the current window * history Returns the History object for the window (See History object) * innerHeight Returns the inner height of a window's content area * innerWidth Returns the inner width of a window's content area * length Returns the number of frames (including iframes) in a window * location Returns the Location object for the window (See Location object) * name Sets or returns the name of a window * navigator Returns the Navigator object for the window (See Navigator object) * opener Returns a reference to the window that created the window * outerHeight Returns the outer height of a window, including toolbars/scrollbars * outerWidth Returns the outer width of a window, including toolbars/scrollbars * pageXOffset Returns the pixels the current document has been scrolled (horizontally) from the upper left corner of the window * pageYOffset Returns the pixels the current document has been scrolled (vertically) from the upper left corner of the window * parent Returns the parent window of the current window * screen Returns the Screen object for the window (See Screen object) * screenLeft Returns the x coordinate of the window relative to the screen * screenTop Returns the y coordinate of the window relative to the screen * screenX Returns the x coordinate of the window relative to the screen * screenY Returns the y coordinate of the window relative to the screen * self Returns the current window * status Sets or returns the text in the statusbar of a window * top Returns the topmost browser window * * Method Description * --- * alert() Displays an alert box with a message and an OK button * atob() Decodes a base-64 encoded string * blur() Removes focus from the current window * btoa() Encodes a string in base-64 * clearInterval() Clears a timer set with setInterval() * clearTimeout() Clears a timer set with setTimeout() * close() Closes the current window * confirm() Displays a dialog box with a message and an OK and a Cancel button * createPopup() Creates a pop-up window * focus() Sets focus to the current window * moveBy() Moves a window relative to its current position * moveTo() Moves a window to the specified position * open() Opens a new browser window * print() Prints the content of the current window * prompt() Displays a dialog box that prompts the visitor for input * resizeBy() Resizes the window by the specified pixels * resizeTo() Resizes the window to the specified width and height * scroll() This method has been replaced by the scrollTo() method. * scrollBy() Scrolls the content by the specified number of pixels * scrollTo() Scrolls the content to the specified coordinates * setInterval() Calls a function or evaluates an expression at specified intervals (in milliseconds) * setTimeout() Calls a function or evaluates an expression after a specified number of milliseconds * stop() Stops the window from loading */ class Web { /** * log(s, type) * * For diagnostic output only. DEBUG must be true (or "--debug" specified via the command-line) * for Component.log() to display anything. * * @param {string} [s] is the message text * @param {string} [type] is the message type */ static log(s, type) { Component.log(s, type); } /** * notice(s, fPrintOnly, id) * * @param {string} s is the message text * @param {boolean} [fPrintOnly] * @param {string} [id] is the caller's ID, if any */ static notice(s, fPrintOnly, id) { Component.notice(s, fPrintOnly, id); } /** * getResource(sURL, dataPost, fAsync, done) * * Request the specified resource (sURL), and once the request is complete, notify done(). * * If fAsync is true, a done() callback should ALWAYS be supplied; otherwise, you'll have no * idea when the request is complete or what the response was. done() is passed three parameters: * * done(sURL, sResource, nErrorCode) * * If nErrorCode is zero, sResource should contain the requested data; otherwise, an error occurred. * * If dataPost is set to a string, that string can be used to control the response format; * by default, the response format is plain text, but you can specify "bytes" to request arbitrary * binary data, which should come back as a string of bytes. * * TODO: The "bytes" option works by calling overrideMimeType(), which was never a best practice. * Instead, we should implement supported response types ("text" and "arraybuffer", at a minimum) * by setting xmlHTTP.responseType to one of those values before calling xmlHTTP.send(). * * @param {string} sURL * @param {string|Object|null} [dataPost] for a POST request (default is a GET request) * @param {boolean} [fAsync] is true for an asynchronous request * @param {function(string,string,number)} [done] * @return {Array|null} Array containing [sResource, nErrorCode], or null if no response yet */ static getResource(sURL, dataPost, fAsync = false, done) { var nErrorCode = 0, sResource = null, response = null; if (typeof resources == 'object' && (sResource = resources[sURL])) { if (done) done(sURL, sResource, nErrorCode); return [sResource, nErrorCode]; } else if (fAsync && typeof resources == 'function') { resources(sURL, function(sResource, nErrorCode) { if (done) done(sURL, sResource, nErrorCode); }); return response; } if (DEBUG) { /* * The larger resources we put on archive.pcjs.org should also be available locally... */ sURL = sURL.replace(/^http:\/\/archive.pcjs.org(\/.*)\/([^\/]*)$/, "$1/archive/$2"); } var xmlHTTP = (window.XMLHttpRequest? new window.XMLHttpRequest() : new window.ActiveXObject("Microsoft.XMLHTTP")); if (fAsync) { xmlHTTP.onreadystatechange = function() { if (xmlHTTP.readyState === 4) { /* * The following line was recommended for WebKit, as a work-around to prevent the handler firing multiple * times when debugging. Unfortunately, that's not the only XMLHttpRequest problem that occurs when * debugging, so I think the WebKit problem is deeper than that. When we have multiple XMLHttpRequests * pending, any debugging activity means most of them simply get dropped on floor, so what may actually be * happening are mis-notifications rather than redundant notifications. * * xmlHTTP.onreadystatechange = undefined; */ sResource = xmlHTTP.responseText; /* * The normal "success" case is an HTTP status code of 200, but when testing with files loaded * from the local file system (ie, when using the "file:" protocol), we have to be a bit more "flexible". */ if (xmlHTTP.status == 200 || !xmlHTTP.status && sResource.length && Web.getHostProtocol() == "file:") { if (MAXDEBUG) Web.log("xmlHTTP.onreadystatechange(" + sURL + "): returned " + sResource.length + " bytes"); } else { nErrorCode = xmlHTTP.status || -1; Web.log("xmlHTTP.onreadystatechange(" + sURL + "): error code " + nErrorCode); } if (done) done(sURL, sResource, nErrorCode); } }; } if (dataPost && typeof dataPost == "object") { var sDataPost = ""; for (var p in dataPost) { if (!dataPost.hasOwnProperty(p)) continue; if (sDataPost) sDataPost += "&"; sDataPost += p + '=' + encodeURIComponent(dataPost[p]); } sDataPost = sDataPost.replace(/%20/g, '+'); if (MAXDEBUG) Web.log("Web.getResource(POST " + sURL + "): " + sDataPost.length + " bytes"); xmlHTTP.open("POST", sURL, fAsync); // ensure that fAsync is a valid boolean (Internet Explorer xmlHTTP functions insist on it) xmlHTTP.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xmlHTTP.send(sDataPost); } else { if (MAXDEBUG) Web.log("Web.getResource(GET " + sURL + ")"); xmlHTTP.open("GET", sURL, fAsync); // ensure that fAsync is a valid boolean (Internet Explorer xmlHTTP functions insist on it) if (dataPost == "bytes") { xmlHTTP.overrideMimeType("text/plain; charset=x-user-defined"); } xmlHTTP.send(); } if (!fAsync) { sResource = xmlHTTP.responseText; if (xmlHTTP.status == 200) { if (MAXDEBUG) Web.log("Web.getResource(" + sURL + "): returned " + sResource.length + " bytes"); } else { nErrorCode = xmlHTTP.status || -1; Web.log("Web.getResource(" + sURL + "): error code " + nErrorCode); } if (done) done(sURL, sResource, nErrorCode); response = [sResource, nErrorCode]; } return response; } /** * parseMemoryResource(sURL, sData) * * This converts a variety of JSON-style data streams into an Object with the following properties: * * aBytes * aSymbols * addrLoad * addrExec * * If the source data contains a 'bytes' array, it's passed through to 'aBytes'; alternatively, if * it contains a 'words' array, the values are converted from 16-bit to 8-bit and stored in 'aBytes', * and if it contains a 'longs' array, the values are converted from 32-bit longs into bytes and * stored in 'aBytes'. * * Alternatively, if the source data contains a 'data' array, we simply pass that through to the output * object as: * * aData * * @param {string} sURL * @param {string} sData * @return {Object|null} (resource) */ static parseMemoryResource(sURL, sData) { var i; var resource = { aBytes: null, aSymbols: null, addrLoad: null, addrExec: null }; if (sData.charAt(0) == "[" || sData.charAt(0) == "{") { try { var a, ib, data; if (sData.substr(0, 1) == "<") { // if the "data" begins with a "<"... /* * Early server configs reported an error (via the nErrorCode parameter) if a tape URL was invalid, * but more recent server configs now display a somewhat friendlier HTML error page. The downside, * however, is that the original error has been buried, and we've received "data" that isn't actually * tape data. So if the data we've received appears to be "HTML-like", we treat it as an error message. */ throw new Error(sData); } /* * TODO: IE9 is rather unfriendly and restrictive with regard to how much data it's willing to * eval(). In particular, the 10Mb disk image we use for the Windows 1.01 demo config fails in * IE9 with an "Out of memory" exception. One work-around would be to chop the data into chunks * (perhaps one track per chunk, using regular expressions) and then manually re-assemble it. * * However, it turns out that using JSON.parse(sDiskData) instead of eval("(" + sDiskData + ")") * is a much easier fix. The only drawback is that we must first quote any unquoted property names * and remove any comments, because while eval() was cool with them, JSON.parse() is more particular; * the following RegExp replacements take care of those requirements. * * The use of hex values is something else that eval() was OK with, but JSON.parse() is not, and * while I've stopped using hex values in DumpAPI responses (at least when "format=json" is specified), * I can't guarantee they won't show up in "legacy" images, and there's no simple RegExp replacement * for transforming hex values into decimal values, so I cop out and fall back to eval() if I detect * any hex prefixes ("0x") in the sequence. Ditto for error messages, which appear like so: * * ["unrecognized disk path: test.img"] */ if (sData.indexOf("0x") < 0 && sData.indexOf("0o") < 0 && sData.substr(0, 2) != '["') { data = JSON.parse(sData.replace(/([a-z]+):/gm, '"$1":').replace(/\/\/[^\n]*/gm, "")); } else { data = eval("(" + sData + ")"); } resource.addrLoad = data['load']; resource.addrExec = data['exec']; if (a = data['bytes']) { resource.aBytes = a; } else if (a = data['words']) { /* * Convert all words into bytes */ resource.aBytes = new Array(a.length * 2); for (i = 0, ib = 0; i < a.length; i++) { resource.aBytes[ib++] = a[i] & 0xff; resource.aBytes[ib++] = (a[i] >> 8) & 0xff; } } else if (a = data['longs']) { /* * Convert all dwords (longs) into bytes */ resource.aBytes = new Array(a.length * 4); for (i = 0, ib = 0; i < a.length; i++) { resource.aBytes[ib++] = a[i] & 0xff; resource.aBytes[ib++] = (a[i] >> 8) & 0xff; resource.aBytes[ib++] = (a[i] >> 16) & 0xff; resource.aBytes[ib++] = (a[i] >> 24) & 0xff; } } else if (a = data['data']) { resource.aData = a; } else { resource.aBytes = data; } if (resource.aBytes) { if (!resource.aBytes.length) { Component.error("Empty resource: " + sURL); resource = null; } else if (resource.aBytes.length == 1) { Component.error(resource.aBytes[0]); resource = null; } } resource.aSymbols = data['symbols']; } catch (e) { Component.error("Resource data error (" + sURL + "): " + e.message); resource = null; } } else { /* * Parse the data manually; we assume it's a series of hex byte-values separated by whitespace. */ var ab = []; var sHexData = sData.replace(/\n/gm, " ").replace(/ +$/, ""); var asHexData = sHexData.split(" "); for (i = 0; i < asHexData.length; i++) { var n = parseInt(asHexData[i], 16); if (isNaN(n)) { Component.error("Resource data error (" + sURL + "): invalid hex byte (" + asHexData[i] + ")"); break; } ab.push(n & 0xff); } if (i == asHexData.length) resource.aBytes = ab; } return resource; } /** * sendReport(sApp, sVer, sURL, sUser, sType, sReport, sHostName) * * Send a report (eg, bug report) to the server. * * @param {string} sApp (eg, "PCjs") * @param {string} sVer (eg, "1.02") * @param {string} sURL (eg, "/devices/pc/machine/5150/mda/64kb/machine.xml") * @param {string} sUser (ie, the user key, if any) * @param {string} sType (eg, "bug"); one of ReportAPI.TYPE.* * @param {string} sReport (eg, unparsed state data) * @param {string} [sHostName] (default is http://SITEHOST) */ static sendReport(sApp, sVer, sURL, sUser, sType, sReport, sHostName) { var dataPost = {}; dataPost[ReportAPI.QUERY.APP] = sApp; dataPost[ReportAPI.QUERY.VER] = sVer; dataPost[ReportAPI.QUERY.URL] = sURL; dataPost[ReportAPI.QUERY.USER] = sUser; dataPost[ReportAPI.QUERY.TYPE] = sType; dataPost[ReportAPI.QUERY.DATA] = sReport; var sReportURL = (sHostName? sHostName : "http://" + SITEHOST) + ReportAPI.ENDPOINT; Web.getResource(sReportURL, dataPost, true); } /** * getHost() * * @return {string} */ static getHost() { return ("http://" + (window? window.location.host : SITEHOST)); } /** * getHostURL() * * @return {string|null} */ static getHostURL() { return (window? window.location.href : null); } /** * getHostProtocol() * * @return {string} */ static getHostProtocol() { return (window? window.location.protocol : "file:"); } /** * getUserAgent() * * @return {string} */ static getUserAgent() { return (window? window.navigator.userAgent : ""); } /** * hasLocalStorage * * true if localStorage support exists, is enabled, and works; false otherwise * * @return {boolean} */ static hasLocalStorage() { if (Web.fLocalStorage == null) { var f = false; if (window) { try { window.localStorage.setItem(Web.sLocalStorageTest, Web.sLocalStorageTest); f = (window.localStorage.getItem(Web.sLocalStorageTest) == Web.sLocalStorageTest); window.localStorage.removeItem(Web.sLocalStorageTest); } catch (e) { Web.logLocalStorageError(e); f = false; } } Web.fLocalStorage = f; } return Web.fLocalStorage; } /** * logLocalStorageError(e) * * @param {Error} e is an exception */ static logLocalStorageError(e) { Web.log(e.message, "localStorage error"); } /** * getLocalStorageItem(sKey) * * Returns the requested key value, or null if the key does not exist, or undefined if localStorage is not available * * @param {string} sKey * @return {string|null|undefined} sValue */ static getLocalStorageItem(sKey) { var sValue; if (window) { try { sValue = window.localStorage.getItem(sKey); } catch (e) { Web.logLocalStorageError(e); } } return sValue; } /** * setLocalStorageItem(sKey, sValue) * * @param {string} sKey * @param {string} sValue * @return {boolean} true if localStorage is available, false if not */ static setLocalStorageItem(sKey, sValue) { try { window.localStorage.setItem(sKey, sValue); return true; } catch (e) { Web.logLocalStorageError(e); } return false; } /** * removeLocalStorageItem(sKey) * * @param {string} sKey */ static removeLocalStorageItem(sKey) { try { window.localStorage.removeItem(sKey); } catch (e) { Web.logLocalStorageError(e); } } /** * getLocalStorageKeys() * * @return {Array} */ static getLocalStorageKeys() { var a = []; try { for (var i = 0, c = window.localStorage.length; i < c; i++) { a.push(window.localStorage.key(i)); } } catch (e) { Web.logLocalStorageError(e); } return a; } /** * reloadPage() */ static reloadPage() { if (window) window.location.reload(); } /** * isUserAgent(s) * * Check the browser's user-agent string for the given substring; "iOS" and "MSIE" are special values you can * use that will match any iOS or MSIE browser, respectively (even IE11, in the case of "MSIE"). * * 2013-11-06: In a questionable move, MSFT changed the user-agent reported by IE11 on Windows 8.1, eliminating * the "MSIE" string (which MSDN calls a "version token"; see http://msdn.microsoft.com/library/ms537503.aspx); * they say "public websites should rely on feature detection, rather than browser detection, in order to design * their sites for browsers that don't support the features used by the website." So, in IE11, we get a user-agent * that tries to fool apps into thinking the browser is more like WebKit or Gecko: * * Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko * * That's a nice idea, but in the meantime, they hosed the XSL transform code in embed.js, which contained * some very critical browser-specific code; turning on IE's "Compatibility Mode" didn't help either, because * that's a sledgehammer solution which restores the old user-agent string but also disables other features like * HTML5 canvas support. As an interim solution, I'm treating any "MSIE" check as a check for either "MSIE" or * "Trident". * * UPDATE: I've since found ways to make the code in embed.js more browser-agnostic, so for now, there's isn't * any code that cares about "MSIE", but I've left the change in place, because I wouldn't be surprised if I'll * need more IE-specific code in the future, perhaps for things like copy/paste functionality, or mouse capture. * * @param {string} s is a substring to search for in the user-agent; as noted above, "iOS" and "MSIE" are special values * @return {boolean} is true if the string was found, false if not */ static isUserAgent(s) { if (window) { var userAgent = Web.getUserAgent(); /* * Here's one case where we have to be careful with Component, because when isUserAgent() is called by * the init code below, component.js hasn't been loaded yet. The simple solution for now is to remove the call. * * Web.log("agent: " + userAgent); * * And yes, it would be pointless to use the conditional (?) operator below, if not for the Google Closure * Compiler (v20130823) failing to detect the entire expression as a boolean. */ return s == "iOS" && !!userAgent.match(/(iPod|iPhone|iPad)/) && !!userAgent.match(/AppleWebKit/) || s == "MSIE" && !!userAgent.match(/(MSIE|Trident)/) || (userAgent.indexOf(s) >= 0); } return false; } /** * isMobile() * * Check the browser's user-agent string for the substring "Mobi", as per Mozilla recommendation: * * https://developer.mozilla.org/en-US/docs/Browser_detection_using_the_user_agent * * @return {boolean} is true if the browser appears to be a mobile (ie, non-desktop) web browser, false if not */ static isMobile() { return Web.isUserAgent("Mobi"); } /** * getURLParm(sParm) * * First looks for sParm exactly as specified, then looks for the lower-case version. * * @param {string} sParm * @return {string|undefined} */ static getURLParm(sParm) { if (!Web.parmsURL) { Web.parmsURL = Web.parseURLParms(); } return Web.parmsURL[sParm] || Web.parmsURL[sParm.toLowerCase()]; } /** * parseURLParms(sParms) * * @param {string} [sParms] containing the parameter portion of a URL (ie, after the '?') * @return {Object} containing properties for each parameter found */ static parseURLParms(sParms) { var aParms = {}; if (window) { // an alternative to "if (typeof module === 'undefined')" if require("defines") was used if (!sParms) { /* * Note that window.location.href returns the entire URL, whereas window.location.search * returns only the parameters, if any (starting with the '?', which we skip over with a substr() call). */ sParms = window.location.search.substr(1); } var match; var pl = /\+/g; // RegExp for replacing addition symbol with a space var search = /([^&=]+)=?([^&]*)/g; var decode = function(s) { return decodeURIComponent(s.replace(pl, " ")); }; while ((match = search.exec(sParms))) { aParms[decode(match[1])] = decode(match[2]); } } return aParms; } /** * downloadFile(sData, sType, fBase64, sFileName) * * @param {string} sData * @param {string} sType * @param {boolean} [fBase64] * @param {string} [sFileName] */ static downloadFile(sData, sType, fBase64, sFileName) { var link = null, sAlert; var sURI = "data:application/" + sType + (fBase64? ";base64" : "") + ","; if (!Web.isUserAgent("Firefox")) { sURI += (fBase64? sData : encodeURI(sData)); } else { sURI += (fBase64? sData : encodeURIComponent(sData)); } if (sFileName) { link = document.createElement('a'); if (typeof link.download != 'string') link = null; } if (link) { link.href = sURI; link.download = sFileName; document.body.appendChild(link); // Firefox allegedly requires the link to be in the body link.click(); document.body.removeChild(link); sAlert = 'Check your Downloads folder for ' + sFileName + '.'; } else { window.open(sURI); sAlert = 'Check your browser for a new window/tab containing the requested data' + (sFileName? (' (' + sFileName + ')') : '') + '.'; } return sAlert; } /** * onCountRepeat(n, fnRepeat, fnComplete, msDelay) * * Call fnRepeat() n times with an msDelay millisecond delay between calls, * then call fnComplete() when n has been exhausted OR fnRepeat() returns false. * * @param {number} n * @param {function()} fnRepeat * @param {function()} fnComplete * @param {number} [msDelay] */ static onCountRepeat(n, fnRepeat, fnComplete, msDelay) { var fnTimeout = function doCountRepeat() { n -= 1; if (n >= 0) { if (!fnRepeat()) n = 0; } if (n > 0) { setTimeout(fnTimeout, msDelay || 0); return; } fnComplete(); }; fnTimeout(); } /** * onClickRepeat(e, msDelay, msRepeat, fn) * * Repeatedly call fn() with an initial msDelay, and an msRepeat delay thereafter, * as long as HTML control Object e has an active "down" event and fn() returns true. * * @param {Object} e * @param {number} msDelay * @param {number} msRepeat * @param {function(boolean)} fn is passed false on the first call, true on all repeated calls */ static onClickRepeat(e, msDelay, msRepeat, fn) { var ms = 0, timer = null, fIgnoreMouseEvents = false; var fnRepeat = function doClickRepeat() { if (fn(ms === msRepeat)) { timer = setTimeout(fnRepeat, ms); ms = msRepeat; } }; e.onmousedown = function() { // Web.log("onMouseDown()"); if (!fIgnoreMouseEvents) { if (!timer) { ms = msDelay; fnRepeat(); } } }; e.ontouchstart = function() { // Web.log("onTouchStart()"); if (!timer) { ms = msDelay; fnRepeat(); } }; e.onmouseup = e.onmouseout = function() { // Web.log("onMouseUp()/onMouseOut()"); if (timer) { clearTimeout(timer); timer = null; } }; e.ontouchend = e.ontouchcancel = function() { // Web.log("onTouchEnd()/onTouchCancel()"); if (timer) { clearTimeout(timer); timer = null; } /* * Devices that generate ontouch* events ALSO generate onmouse* events, * and generally do so immediately after all the touch events are complete, * so unless we want double the action, we need to ignore mouse events. */ fIgnoreMouseEvents = true; }; } /** * onPageEvent(sName, fn) * * For 'onload', 'onunload', and 'onpageshow' events, most callers should NOT use this function, but * instead use Web.onInit(), Web.onShow(), and Web.onExit(), respectively. * * The only components that should still use onPageEvent() are THIS component (see the bottom of this file) * and components that need to capture other events (eg, the 'onresize' event in the Video component). * * This function creates a chain of callbacks, allowing multiple JavaScript modules to define handlers * for the same event, which wouldn't be possible if everyone modified window['onload'], window['onunload'], * etc, themselves. However, that's less of a concern now, because assuming everyone else is now using * onInit(), onExit(), etc, then there really IS only one component setting the window callback: this one. * * NOTE: It's risky to refer to obscure event handlers with "dot" names, because the Closure Compiler may * erroneously replace them (eg, window.onpageshow is a good example). * * @param {string} sFunc * @param {function()} fn */ static onPageEvent(sFunc, fn) { if (window) { var fnPrev = window[sFunc]; if (typeof fnPrev !== 'function') { window[sFunc] = fn; } else { /* * TODO: Determine whether there's any value in receiving/sending the Event object that the * browser provides when it generates the original event. */ window[sFunc] = function onWindowEvent() { if (fnPrev) fnPrev(); fn(); }; } } }; /** * onInit(fn) * * Use this instead of setting window.onload. Allows multiple JavaScript modules to define their own 'onload' event handler. * * @param {function()} fn */ static onInit(fn) { Web.aPageEventHandlers['init'].push(fn); }; /** * onShow(fn) * * @param {function()} fn * * Use this instead of setting window.onpageshow. Allows multiple JavaScript modules to define their own 'onpageshow' event handler. */ static onShow(fn) { Web.aPageEventHandlers['show'].push(fn); }; /** * onExit(fn) * * @param {function()} fn * * Use this instead of setting window.onunload. Allows multiple JavaScript modules to define their own 'onunload' event handler. */ static onExit(fn) { Web.aPageEventHandlers['exit'].push(fn); }; /** * doPageEvent(afn) * * @param {Array.} afn */ static doPageEvent(afn) { if (Web.fPageEventsEnabled) { try { for (var i = 0; i < afn.length; i++) { afn[i](); } } catch (e) { Web.notice("An unexpected exception occurred:\n\n" + e.message + "\n\nPlease send this information to support@pcjs.org. Thanks."); } } }; /** * enablePageEvents(fEnable) * * @param {boolean} fEnable is true to enable page events, false to disable (they're enabled by default) */ static enablePageEvents(fEnable) { if (!Web.fPageEventsEnabled && fEnable) { Web.fPageEventsEnabled = true; if (Web.fPageLoaded) Web.sendPageEvent('init'); if (Web.fPageShowed) Web.sendPageEvent('show'); return; } Web.fPageEventsEnabled = fEnable; } /** * sendPageEvent(sEvent) * * This allows us to manually trigger page events. * * @param {string} sEvent (one of 'init', 'show' or 'exit') */ static sendPageEvent(sEvent) { if (Web.aPageEventHandlers[sEvent]) { Web.doPageEvent(Web.aPageEventHandlers[sEvent]); } } } Web.parmsURL = null; // initialized on first call to parseURLParms() Web.aPageEventHandlers = { 'init': [], // list of window 'onload' handlers 'show': [], // list of window 'onpageshow' handlers 'exit': [] // list of window 'onunload' handlers (although we prefer to use 'onbeforeunload' if possible) }; Web.fPageLoaded = false; // set once the page's first 'onload' event has occurred Web.fPageShowed = false; // set once the page's first 'onpageshow' event has occurred Web.fPageEventsEnabled = true; // default is true, set to false (or true) by enablePageEvents() /** * fLocalStorage * * true if localStorage support exists, is enabled, and works; "falsey" otherwise * * @type {boolean|null} */ Web.fLocalStorage = null; /** * TODO: Is there any way to get the Closure Compiler to stop inlining this string? This isn't cutting it. * * @const {string} */ Web.sLocalStorageTest = "PCjs.localStorage"; Web.onPageEvent('onload', function onPageLoad() { Web.fPageLoaded = true; Web.doPageEvent(Web.aPageEventHandlers['init']); }); Web.onPageEvent('onpageshow', function onPageShow() { Web.fPageShowed = true; Web.doPageEvent(Web.aPageEventHandlers['show']); }); Web.onPageEvent(Web.isUserAgent("iOS")? 'onpagehide' : (Web.isUserAgent("Opera")? 'onunload' : 'onbeforeunload'), function onPageUnload() { Web.doPageEvent(Web.aPageEventHandlers['exit']); }); /** * @copyright http://pcjs.org/modules/shared/lib/component.js (C) Jeff Parsons 2012-2017 */ /* * All PCjs components now use JSDoc types, primarily so that Google's Closure Compiler will compile * everything with zero warnings when ADVANCED_OPTIMIZATIONS are enabled. For more information about * the JSDoc types supported by the Closure Compiler: * * https://developers.google.com/closure/compiler/docs/js-for-compiler#types * * I also attempted to validate this code with JSLint, but it complained too much; eg, it didn't like * "while (true)", a tried and "true" programming convention for decades, and it wanted me to replace * all "++" and "--" operators with "+= 1" and "-= 1", use "(s || '')" instead of "(s? s : '')", etc. * * I prefer sticking with traditional C-style idioms, in part because they are more portable. That * does NOT mean I'm trying to write "portable JavaScript," but some of this code was ported from C code * I'd written long ago, so portability is good, and I'm not going to throw that away if there's no need. * * UPDATE: I've since switched from JSLint to JSHint, which seems to have more reasonable defaults. * And for new code, I have adopted some popular JavaScript idioms, like "(s || '')", although the need * for those kinds of expressions will be reduced as I also start adopting some ES6 features, like * default parameters. */ /** * Since the Closure Compiler treats ES6 classes as @struct rather than @dict by default, * it deters us from defining named properties on our components; eg: * * this['exports'] = {...} * * results in an error: * * Cannot do '[]' access on a struct * * So, in order to define 'exports', we must override the @struct assumption by annotating * the class as @unrestricted (or @dict). Note that this must be done both here and in the * subclass (eg, SerialPort), because otherwise the Compiler won't allow us to *reference* * the named property either. * * TODO: Consider marking ALL our classes unrestricted, because otherwise it forces us to * define every single property the class uses in its constructor, which results in a fair * bit of redundant initialization, since many properties aren't (and don't need to be) fully * initialized until the appropriate init(), reset(), restore(), etc. function is called. * * The upside, however, may be that since the structure of the class is completely defined by * the constructor, JavaScript engines may be able to optimize and run more efficiently. * * @unrestricted */ class Component { /** * Component(type, parms, bitsMessage) * * A Component object requires: * * type: a user-defined type name (eg, "CPU") * * and accepts any or all of the following (parms) properties: * * id: component ID (default is "") * name: component name (default is ""; if blank, toString() will use the type name only) * comment: component comment string (default is undefined) * * Component subclasses will usually have additional (parms) properties. * * @param {string} type * @param {Object} [parms] * @param {number} [bitsMessage] selects message(s) that the component wants to enable (default is 0) */ constructor(type, parms, bitsMessage) { this.type = type; if (!parms) parms = {'id': "", 'name': ""}; this.id = parms['id'] || ""; this.name = parms['name']; this.comment = parms['comment']; this.parms = parms; /* * The following Component properties need to be accessible by other machines and/or command scripts; * well, OK, or we could have exported some new functions to walk the contents of these properties, as we * did with findMachineComponent(), but this works just as well. * * Also, while the double-assignment looks silly (ie, using both dot and bracket property notation), it * resolves a complaint from the Closure Compiler, because if we use ONLY bracket notation here, then the * Compiler wants us to change all the other references to bracket notation as well. */ this.exports = this['exports'] = {}; this.bindings = this['bindings'] = {}; var i = this.id.indexOf('.'); if (i < 0) { this.idComponent = this.id; } else { this.idMachine = this.id.substr(0, i); this.idComponent = this.id.substr(i + 1); } /* * Gather all the various component flags (booleans) into a single "flags" object, and encourage * subclasses to do the same, to reduce the property clutter we have to wade through while debugging. */ this.flags = { ready: false, busy: false, busyCancel: false, powered: false, unloading: false, error: false }; this.fnReady = null; this.clearError(); this.bitsMessage = bitsMessage || 0; /** @type {Object|null} controlPrint is the HTML control, if any, that we can print to */ this.controlPrint = null; this.cmp = null; this.bus = null; this.cpu = null; this.dbg = null; /* * TODO: Consider adding another parameter to the Component() constructor that allows components to tell * us if they support single or multiple instances per machine. For example, there can be multiple SerialPort * components per machine, but only one CPU component (some machines also support an FPU, but that component * is considered separate from the CPU). * * It's not critical, but it would help catch machine configuration errors; for example, a machine that mistakenly * includes two CPU components may, aside from wasting memory, end up with odd side-effects, like unresponsive * CPU controls. */ Component.add(this); } /** * Component.add(component) * * @param {Component} component */ static add(component) { /* * This just generates a lot of useless noise, handy in the early days, not so much these days.... * * if (DEBUG) Component.log("Component.add(" + component.type + "," + component.id + ")"); */ Component.components.push(component); } /** * Component.addMachine(idMachine) * * @param {string} idMachine */ static addMachine(idMachine) { Component.machines[idMachine] = {}; } /** * Component.addMachineResource(idMachine, sName, data) * * @param {string} idMachine * @param {string|null} sName (name of the resource) * @param {*} data */ static addMachineResource(idMachine, sName, data) { /* * I used to assert(Component.machines[idMachine]), but when we're running as a Node app, embed.js is not used, * so addMachine() is never called, so resources do not need to be recorded. */ if (Component.machines[idMachine] && sName) { Component.machines[idMachine][sName] = data; } } /** * Component.getMachineResources(idMachine) * * @param {string} idMachine * @return {Object|undefined} */ static getMachineResources(idMachine) { return Component.machines[idMachine]; } /** * Component.getTime() * * @return {number} the current time, in milliseconds */ static getTime() { return Date.now() || +new Date(); } /** * Component.log(s, type) * * For diagnostic output only. * * @param {string} [s] is the message text * @param {string} [type] is the message type */ static log(s, type) { if (!COMPILED) { if (s) { var sElapsed = "", sMsg = (type? (type + ": ") : "") + s; if (typeof Usr != "undefined") { if (Component.msStart === undefined) { Component.msStart = Component.getTime(); } sElapsed = (Component.getTime() - Component.msStart) + "ms: "; } if (window && window.console) console.log(sElapsed + sMsg.replace(/\n/g, " ")); } } } /** * Component.assert(f, s) * * Verifies conditions that must be true (for DEBUG builds only). * * The Closure Compiler should automatically remove all references to Component.assert() in non-DEBUG builds. * TODO: Add a task to the build process that "asserts" there are no instances of "assertion failure" in RELEASE builds. * * @param {boolean} f is the expression we are asserting to be true * @param {string} [s] is description of the assertion on failure */ static assert(f, s) { if (DEBUG) { if (!f) { if (!s) s = "assertion failure"; Component.log(s); throw new Error(s); } } } /** * Component.println(s, type, id) * * For non-diagnostic messages, which components may override to control the destination/appearance of their output. * * Components that inherit from this class should use the instance method, this.println(), rather than Component.println(), * because if a Control Panel is loaded, it will override only the instance method, not the class method (overriding the class * method would improperly affect any other machines loaded on the same page). * * @param {string} [s] is the message text * @param {string} [type] is the message type * @param {string} [id] is the caller's ID, if any */ static println(s, type, id) { if (!COMPILED) { Component.log((id? (id + ": ") : "") + (s? ("\"" + s + "\"") : ""), type); } } /** * Component.notice(s, fPrintOnly, id) * * notice() is like println() but implies a need for user notification, so we alert() as well. * * @param {string} s is the message text * @param {boolean} [fPrintOnly] * @param {string} [id] is the caller's ID, if any */ static notice(s, fPrintOnly, id) { if (!COMPILED) { Component.println(s, "notice", id); } if (!fPrintOnly) Component.alertUser((id? (id + ": ") : "") + s); } /** * Component.warning(s) * * @param {string} s describes the warning */ static warning(s) { if (!COMPILED) { Component.println(s, "warning"); } Component.alertUser(s); } /** * Component.error(s) * * @param {string} s describes the error; an alert() is displayed as well */ static error(s) { if (!COMPILED) { Component.println(s, "error"); } Component.alertUser(s); } /** * Component.alertUser(sMessage) * * @param {string} sMessage */ static alertUser(sMessage) { if (window) { window.alert(sMessage); } else { Component.log(sMessage); } }; /** * Component.confirmUser(sPrompt) * * @param {string} sPrompt * @returns {boolean} true if the user clicked OK, false if Cancel/Close */ static confirmUser(sPrompt) { var fResponse = false; if (window) { fResponse = window.confirm(sPrompt); } return fResponse; } /** * Component.promptUser() * * @param {string} sPrompt * @param {string} [sDefault] * @returns {string|null} */ static promptUser(sPrompt, sDefault) { var sResponse = null; if (window) { sResponse = window.prompt(sPrompt, sDefault === undefined? "" : sDefault); } return sResponse; } /** * Component.getComponents(idRelated) * * We could store components as properties, using the component's ID, and change * this linear lookup into a property lookup, but some components may have no ID. * * @param {string} [idRelated] of related component * @return {Array} of components */ static getComponents(idRelated) { var i; var aComponents = []; /* * getComponentByID(id, idRelated) * * If idRelated is provided, we check it for a machine prefix, and use any * existing prefix to constrain matches to IDs with the same prefix, in order to * avoid matching components belonging to other machines. */ if (idRelated) { if ((i = idRelated.indexOf('.')) > 0) idRelated = idRelated.substr(0, i + 1); else idRelated = ""; } for (i = 0; i < Component.components.length; i++) { var component = Component.components[i]; if (!idRelated || !component.id.indexOf(idRelated)) { aComponents.push(component); } } return aComponents; } /** * Component.getComponentByID(id, idRelated) * * We could store components as properties, using the component's ID, and change * this linear lookup into a property lookup, but some components may have no ID. * * @param {string} id of the desired component * @param {string} [idRelated] of related component * @return {Component|null} */ static getComponentByID(id, idRelated) { if (id !== undefined) { var i; /* * If idRelated is provided, we check it for a machine prefix, and use any * existing prefix to constrain matches to IDs with the same prefix, in order to * avoid matching components belonging to other machines. */ if (idRelated && (i = idRelated.indexOf('.')) > 0) { id = idRelated.substr(0, i + 1) + id; } for (i = 0; i < Component.components.length; i++) { if (Component.components[i].id === id) { return Component.components[i]; } } if (Component.components.length) { Component.log("Component ID '" + id + "' not found", "warning"); } } return null; } /** * Component.getComponentByType(sType, idRelated, componentPrev) * * @param {string} sType of the desired component * @param {string} [idRelated] of related component * @param {Component|null} [componentPrev] of previously returned component, if any * @return {Component|null} */ static getComponentByType(sType, idRelated, componentPrev) { if (sType !== undefined) { var i; /* * If idRelated is provided, we check it for a machine prefix, and use any * existing prefix to constrain matches to IDs with the same prefix, in order to * avoid matching components belonging to other machines. */ if (idRelated) { if ((i = idRelated.indexOf('.')) > 0) { idRelated = idRelated.substr(0, i + 1); } else { idRelated = ""; } } for (i = 0; i < Component.components.length; i++) { if (componentPrev) { if (componentPrev == Component.components[i]) componentPrev = null; continue; } if (sType == Component.components[i].type && (!idRelated || !Component.components[i].id.indexOf(idRelated))) { return Component.components[i]; } } Component.log("Component type '" + sType + "' not found", "warning"); } return null; } /** * Component.getComponentParms(element) * * @param {Object} element from the DOM */ static getComponentParms(element) { var parms = null; var sParms = element.getAttribute("data-value"); if (sParms) { try { parms = eval("(" + sParms + ")"); // jshint ignore:line /* * We can no longer invoke removeAttribute() because some components (eg, Panel) need * to run their initXXX() code more than once, to avoid initialization-order dependencies. * * if (!DEBUG) { * element.removeAttribute("data-value"); * } */ } catch(e) { Component.error(e.message + " (" + sParms + ")"); } } return parms; } /** * Component.bindExternalControl(component, sControl, sBinding, sType) * * @param {Component} component * @param {string} sControl * @param {string} sBinding * @param {string} [sType] is the external component type */ static bindExternalControl(component, sControl, sBinding, sType) { if (sControl) { if (sType === undefined) sType = "Panel"; var target = Component.getComponentByType(sType, component.id); if (target) { var eBinding = target.bindings[sControl]; if (eBinding) { component.setBinding(null, sBinding, eBinding); } } } } /** * Component.bindComponentControls(component, element, sAppClass) * * @param {Component} component * @param {Object} element from the DOM * @param {string} sAppClass */ static bindComponentControls(component, element, sAppClass) { var aeControls = Component.getElementsByClass(element.parentNode, sAppClass + "-control"); for (var iControl = 0; iControl < aeControls.length; iControl++) { var aeChildNodes = aeControls[iControl].childNodes; for (var iNode = 0; iNode < aeChildNodes.length; iNode++) { var control = aeChildNodes[iNode]; if (control.nodeType !== 1 /* document.ELEMENT_NODE */) { continue; } var sClass = control.getAttribute("class"); if (!sClass) continue; var aClasses = sClass.split(" "); for (var iClass = 0; iClass < aClasses.length; iClass++) { var parms; sClass = aClasses[iClass]; switch (sClass) { case sAppClass + "-binding": parms = Component.getComponentParms(control); if (parms && parms['binding']) { component.setBinding(parms['type'], parms['binding'], control, parms['value']); } else if (!parms || parms['type'] != "description") { Component.log("Component '" + component.toString() + "' missing binding" + (parms? " for " + parms['type'] : ""), "warning"); } iClass = aClasses.length; break; default: // if (DEBUG) Component.log("Component.bindComponentControls(" + component.toString() + "): unrecognized control class \"" + sClass + "\"", "warning"); break; } } } } } /** * Component.getElementsByClass(element, sClass, sObjClass) * * This is a cross-browser helper function, since not all browser's support getElementsByClassName() * * TODO: This should probably be moved into weblib.js at some point, along with the control binding functions above, * to keep all the browser-related code together. * * @param {Object} element from the DOM * @param {string} sClass * @param {string} [sObjClass] * @return {Array|NodeList} */ static getElementsByClass(element, sClass, sObjClass) { if (sObjClass) sClass += '-' + sObjClass + "-object"; /* * Use the browser's built-in getElementsByClassName() if it appears to be available * (for example, it's not available in IE8, but it should be available in IE9 and up) */ if (element.getElementsByClassName) { return element.getElementsByClassName(sClass); } var i, j, ae = []; var aeAll = element.getElementsByTagName("*"); var re = new RegExp('(^| )' + sClass + '( |$)'); for (i = 0, j = aeAll.length; i < j; i++) { if (re.test(aeAll[i].className)) { ae.push(aeAll[i]); } } if (!ae.length) { Component.log('No elements of class "' + sClass + '" found'); } return ae; } /** * Component.getScriptCommands(sScript) * * This is a simple parser that breaks sScript into an array of commands, where each command * is an array of tokens, where tokens are sequences of characters separated by any of: tab, space, * carriage-return (CR), line-feed (LF), semicolon, single-quote, or double-quote; if a quote is * used, all characters up to the next matching quote become part of the token, allowing any of the * other separators to be part of the token. CR, LF and semicolon also serve to terminate a command, * with semicolon being preferred, because it's 1) more visible, and 2) essential when the entire * script is a multi-line string where all CR/LF were replaced by spaces (which is what Jekyll does, * and since we can't change Jekyll, it's what our own MarkDown Front Matter parser does as well; * see convertMD() in markout.js, where the aCommandDefs array is built). * * Backslash sequences like \n, \r, and \\ have already been converted to LF, CR and backslash * characters, since the entire script string is injected into a JavaScript function call, so any * backslash sequence that JavaScript supports is automatically converted: * * \0 \' \" \\ \n \r \v \t \b \f \uXXXX \xXX * ^J ^M ^K ^I ^H ^L * * To support any other non-printable 8-bit character, such as ESC, you should use \xXX, where XX * is the ASCII code in hex. For ESC, that would \x1B. * * @param {string} sScript * @return {Array} */ static getScriptCommands(sScript) { var cch = sScript.length; var aCommands = [], aTokens = [], sToken = "", chQuote = null; for (var i = 0; i < cch; i++) { var ch = sScript[i]; if (ch == '"' || ch == "'") { if (chQuote && ch != chQuote) { sToken += ch; continue; } if (!chQuote) { chQuote = ch; } else { chQuote = null; } if (sToken) { aTokens.push(sToken); sToken = ""; } continue; } if (!chQuote) { if (ch == '\r' || ch == '\n') { ch = ';'; } if (ch == ' ' || ch == '\t' || ch == ';') { if (sToken) { aTokens.push(sToken); sToken = ""; } if (ch == ';' && aTokens.length) { aCommands.push(aTokens); aTokens = []; } continue; } } sToken += ch; } if (sToken) { aTokens.push(sToken); } if (aTokens.length) { aCommands.push(aTokens); } return aCommands; } /** * Component.processScript(idMachine, sScript) * * @param {string} idMachine * @param {string} sScript * @return {boolean} */ static processScript(idMachine, sScript) { var fSuccess = false; idMachine += ".machine"; if (typeof sScript == "string" && !Component.commands[idMachine]) { fSuccess = true; Component.commands[idMachine] = Component.getScriptCommands(sScript); if (!Component.processCommands(idMachine)) { fSuccess = false; } } return fSuccess; } /** * Component.processCommands(idMachine) * * @param {string} idMachine * @return {boolean} */ static processCommands(idMachine) { var fSuccess = true; var aCommands = Component.commands[idMachine]; // var dbg = Component.getComponentByType("Debugger", idMachine); while (aCommands && aCommands.length) { var aTokens = aCommands.splice(0, 1)[0]; var sCommand = aTokens[0]; /* * It's possible to route this output to the Debugger window with dbg.println() * instead, but it's a bit too confusing mingling script output in a window that * already mingles Debugger and machine output. */ Component.println('script: ' + aTokens.join(' ')); var fnCallReady = null; if (Component.asyncCommands.indexOf(sCommand) >= 0) { fnCallReady = function processNextCommand() { return function() { Component.processCommands(idMachine); } }(); } var fnCommand = Component.globalCommands[sCommand]; if (fnCommand) { if (!fnCallReady) { fSuccess = fnCommand(aTokens[1], aTokens[2], aTokens[3]); } else { if (!fnCommand(fnCallReady, aTokens[1], aTokens[2], aTokens[3])) break; } } else { fSuccess = false; var component = Component.getComponentByType(aTokens[1], idMachine); if (component) { fnCommand = Component.componentCommands[sCommand]; if (fnCommand) { fSuccess = fnCommand(component, aTokens[2], aTokens[3]); } else { var exports = component['exports']; if (exports) { fnCommand = exports[sCommand]; if (fnCommand) { fSuccess = true; if (!fnCallReady) { fSuccess = fnCommand.call(component, aTokens[2], aTokens[3]); } else { if (!fnCommand.call(component, fnCallReady, aTokens[2], aTokens[3])) break; } } } } } } if (!fSuccess) { Component.alertUser("Script error: " + sCommand + (fnCommand? " failed" : " unrecognized")); break; } } if (aCommands && !aCommands.length) { delete Component.commands[idMachine]; } return fSuccess; } /** * Component.scriptAlert(sMessage) * * @param {string} sMessage * @return {boolean} */ static scriptAlert(sMessage) { Component.alertUser(sMessage); return true; } /** * Component.scriptSelect(component, sBinding, sValue) * * @param {Component} component * @param {string} sBinding * @param {string} sValue * @return {boolean} */ static scriptSelect(component, sBinding, sValue) { var fSuccess = false; var aBindings = component['bindings']; var control = aBindings[sBinding]; if (control) { for (var i = 0; i < control.options.length; i++) { if (control.options[i].textContent == sValue) { if (control.selectedIndex != i) { control.selectedIndex = i; } fSuccess = true; break; } } } return fSuccess; } /** * Component.scriptSleep(fnCallback, sDelay) * * @param {function()} fnCallback * @param {string} sDelay (in milliseconds) * @return {boolean} */ static scriptSleep(fnCallback, sDelay) { setTimeout(fnCallback, +sDelay); return false; } /** * toString() * * @this {Component} * @return {string} */ toString() { return (this.name? this.name : (this.id || this.type)); } /** * getMachineNum() * * @this {Component} * @return {number} unique machine number */ getMachineNum() { var nMachine = 1; if (this.idMachine) { var aDigits = this.idMachine.match(/\d+/); if (aDigits !== null) nMachine = parseInt(aDigits[0], 10); } return nMachine; } /** * setBinding(sHTMLType, sBinding, control, sValue) * * Component's setBinding() method is intended to be overridden by subclasses. * * @this {Component} * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "reset") * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) * @param {string} [sValue] optional data value * @return {boolean} true if binding was successful, false if unrecognized binding request */ setBinding(sHTMLType, sBinding, control, sValue) { switch (sBinding) { case "clear": if (!this.bindings[sBinding]) { this.bindings[sBinding] = control; control.onclick = (function(component) { return function clearPanel() { if (component.bindings['print']) { component.bindings['print'].value = ""; } }; }(this)); } return true; case "print": if (!this.bindings[sBinding]) { this.bindings[sBinding] = control; /* * HACK: Save this particular HTML element so that the Debugger can access it, too */ this.controlPrint = control; /* * This was added for Firefox (Safari automatically clears the