// Produce an async function from a source string if (!globalThis.AsyncFunction) globalThis.AsyncFunction = Object.getPrototypeOf(async function(){}).constructor; // Read scripts from files on disk to aid with debugging let debug = location.hash == "#debug"; /////////////////////////////////////////////////////////////////////////////// // Bundle // /////////////////////////////////////////////////////////////////////////////// // Resource asset manager globalThis.Bundle = class BundledFile { ///////////////////////////// Static Methods ////////////////////////////// // Adds a bundled file from loaded file data static add(name, data) { return Bundle.files[name] = new Bundle(name, data); } // Retrieve the file given its filename static get(name) { return Bundle.files[name]; } // Run a file as a JavaScript source file static async run(name) { await Bundle.files[name].run(); } // Resolve a URL for a source file static source(name) { return debug ? name : Bundle.files[name].toDataURL(); } ///////////////////////// Initialization Methods ////////////////////////// // Object constructor constructor(name, data) { // Configure instance fields this.data = data; this.name = name; // Detect the MIME type this.mime = name.endsWith(".css" ) ? "text/css;charset=UTF-8" : name.endsWith(".frag" ) ? "text/plain;charset=UTF-8" : name.endsWith(".js" ) ? "text/javascript;charset=UTF-8" : name.endsWith(".png" ) ? "image/png" : name.endsWith(".svg" ) ? "image/svg+xml;charset=UTF-8" : name.endsWith(".txt" ) ? "text/plain;charset=UTF-8" : name.endsWith(".vert" ) ? "text/plain;charset=UTF-8" : name.endsWith(".wasm" ) ? "application/wasm" : name.endsWith(".woff2") ? "font/woff2" : "application/octet-stream" ; } ///////////////////////////// Public Methods ////////////////////////////// // Execute the file as a JavaScript source file async run() { // Not running in debug mode if (!debug) { await new AsyncFunction(this.toString())(); return; } // Running in debug mode await new Promise((resolve,reject)=>{ let script = document.createElement("script"); document.head.appendChild(script); script.addEventListener("load", ()=>resolve()); script.src = this.name; }); } // Register the file as a CSS stylesheet style(enabled) { let link = document.createElement("link"); link.href = debug ? this.name : this.toDataURL(); link.rel = "stylesheet"; link.type = "text/css"; link.setEnabled = enabled=>{ if (enabled) link.removeAttribute("disabled"); else link.setAttribute("disabled", null); }; link.setEnabled(enabled === undefined || !!enabled); document.head.appendChild(link); return link; } // Produce a blob from the file data toBlob() { return new Blob(this.data, { type: this.mime }); } // Produce a blob URL for the file data toBlobURL() { return URL.createObjectURL(this.toBlob()); } // Encode the file data as a data URL toDataURL() { return "data:" + this.mime + ";base64," + btoa(this.toString()); } // Decode the file data as a UTF-8 string toString() { return new TextDecoder().decode(this.data); } }; Bundle.files = []; /////////////////////////////////////////////////////////////////////////////// // ZIP Bundler // /////////////////////////////////////////////////////////////////////////////// // Data buffer utility processor class Bin { // Object constructor constructor() { this.data = []; this.offset = 0; } ///////////////////////////// Public Methods ////////////////////////////// // Convert the data contents to a byte array toByteArray() { return Uint8Array.from(this.data); } // Encode a byte array writeBytes(data) { this.data = this.data.concat(Array.from(data)); } // Encode a sized integer writeInt(length, value) { for (value &= 0xFFFFFFFF; length > 0; length--, value >>>= 8) this.data.push(value & 0xFF); } // Encode a string as UTF-8 with prepended length writeString(value) { this.writeBytes(new TextEncoder().encode(value)); } } // Generate the CRC32 lookup table let crcLookup = new Uint32Array(256); for (let x = 0; x <= 255; x++) { let l = x; for (let j = 7; j >= 0; j--) l = ((l >>> 1) ^ (0xEDB88320 & -(l & 1))); crcLookup[x] = l; } // Calculate the CRC32 checksum for a byte array function crc32(data) { let c = 0xFFFFFFFF; for (let x = 0; x < data.length; x++) c = ((c >>> 8) ^ crcLookup[(c ^ data[x]) & 0xFF]); return ~c & 0xFFFFFFFF; } // Produce a .ZIP header from a bundled file function toZipHeader(file, crc32, offset) { let central = offset || offset === 0; let ret = new Bin(); if (central) { ret.writeInt (4, 0x02014B50); // Signature ret.writeInt (2, 20); // Version created by } else ret.writeInt (4, 0x04034B50); // Signature ret.writeInt (2, 20); // Version required ret.writeInt (2, 0); // Bit flags ret.writeInt (2, 0); // Compression method ret.writeInt (2, 0); // Modified time ret.writeInt (2, 0); // Modified date ret.writeInt (4, crc32); // Checksum ret.writeInt (4, file.data.length); // Compressed size ret.writeInt (4, file.data.length); // Uncompressed size ret.writeInt (2, file.name.length); // Filename length ret.writeInt (2, 0); // Extra field length if (central) { ret.writeInt (2, 0); // File comment length ret.writeInt (2, 0); // Disk number start ret.writeInt (2, 0); // Internal attributes ret.writeInt (4, 0); // External attributes ret.writeInt (4, offset); // Relative offset } ret.writeString (file.name, true); // Filename if (!central) ret.writeBytes(file.data); // File data return ret.toByteArray(); } // Package all bundled files into a .zip file for download Bundle.save = function() { let centrals = new Array(manifest.length); let locals = new Array(manifest.length); let offset = 0; let size = 0; // Encode file and directory entries let keys = Object.keys(Bundle.files); for (let x = 0; x < keys.length; x++) { let file = Bundle.get(keys[x]); let sum = crc32(file.data); locals [x] = toZipHeader(file, sum); centrals[x] = toZipHeader(file, sum, offset); offset += locals [x].length; size += centrals[x].length; } // Encode end of central directory let end = new Bin(); end.writeInt(4, 0x06054B50); // Signature end.writeInt(2, 0); // Disk number end.writeInt(2, 0); // Central dir start disk end.writeInt(2, centrals.length); // # central dir this disk end.writeInt(2, centrals.length); // # central dir total end.writeInt(4, size); // Size of central dir end.writeInt(4, offset); // Offset of central dir end.writeInt(2, 0); // .ZIP comment length // Prompt the user to save the resulting file let a = document.createElement("a"); a.download = bundleName + ".zip"; a.href = URL.createObjectURL(new Blob( locals.concat(centrals).concat([end.toByteArray()]), { type: "application/zip" } )); a.click(); } /////////////////////////////////////////////////////////////////////////////// // Boot Loader // /////////////////////////////////////////////////////////////////////////////// /* The Bundle.java utility prepends a manifest object to _boot.js that is an array of file infos, each of which has a name and size property. */ // Remove the bundle image element from the document arguments[0].remove(); // Process all files from the bundle blob let blob = arguments[1]; let offset = arguments[2] - manifest[0].size * 4; for (let file of manifest) { let data = new Uint8Array(file.size); for (let x = 0; x < file.size; x++, offset += 4) data[x] = blob[offset]; if (file == manifest[0]) offset += 4; Bundle.add(file.name, data); } // Program startup let run = async function() { // Fonts for (let file of Object.values(Bundle.files)) { if (!file.name.endsWith(".woff2")) continue; let family = "/" + file.name; family = family.substring(family.lastIndexOf("/")+1, family.length-6); let font = new FontFace(family, file.data); await font.load(); document.fonts.add(font); } // Scripts await Bundle.run("app/App.js"); await Bundle.run("app/Debugger.js"); await Bundle.run("app/toolkit/Toolkit.js"); await Bundle.run("app/toolkit/Component.js"); await Bundle.run("app/toolkit/Panel.js"); await Bundle.run("app/toolkit/Application.js"); await Bundle.run("app/toolkit/Button.js"); await Bundle.run("app/toolkit/ButtonGroup.js"); await Bundle.run("app/toolkit/CheckBox.js"); await Bundle.run("app/toolkit/Label.js"); await Bundle.run("app/toolkit/MenuBar.js"); await Bundle.run("app/toolkit/MenuItem.js"); await Bundle.run("app/toolkit/Menu.js"); await Bundle.run("app/toolkit/RadioButton.js"); await Bundle.run("app/toolkit/Splitter.js"); await Bundle.run("app/toolkit/TextBox.js"); await Bundle.run("app/toolkit/Window.js"); await Bundle.run("app/windows/CPUWindow.js"); await Bundle.run("app/windows/Disassembler.js"); await Bundle.run("app/windows/Register.js"); await Bundle.run("app/windows/MemoryWindow.js"); await App.create(); }; run();