324 lines
10 KiB
JavaScript
324 lines
10 KiB
JavaScript
// 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/Register.js");
|
|
await Bundle.run("app/windows/MemoryWindow.js");
|
|
await App.create();
|
|
};
|
|
run();
|