diff --git a/Build.app.htm b/Build.app.htm
new file mode 100644
index 0000000..0461f39
--- /dev/null
+++ b/Build.app.htm
@@ -0,0 +1,5685 @@
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
+///
diff --git a/README.markdown b/README.markdown
index e5127a5..675a764 100644
--- a/README.markdown
+++ b/README.markdown
@@ -1 +1,14 @@
xxd in the browser. (Default behavior only.)
+
+
+Building
+========
+
+Using `Build.app.htm` (for example, after double clicking it and then pointing
+it at the project source tree), build trplkt by running the following command:
+
+ build ./main.src
+
+(If you prefer to do the build from the terminal, you can also do so using
+NodeJS; you'd type `node ./Build.app.htm $BUILDCOMMAND`, where `$BUILDCOMMAND`
+is the command given above.)
diff --git a/main.src b/main.src
new file mode 100644
index 0000000..92844d0
--- /dev/null
+++ b/main.src
@@ -0,0 +1,23 @@
+import { XXD } from "./src/XXD.src"
+
+import { SystemB } from "./src/SystemB.src"
+import { SystemC } from "./src/SystemC.src"
+
+void function main() {
+ if (!system) var system = SystemA.init(SystemB);
+ if (!system) var system = SystemA.init(SystemC);
+
+ if (!system) {
+ throw new Error("Unknown environment");
+ }
+
+ system.setUpForExec({
+ run: function(system, path) {
+ return system.read(path).then((contents) => {
+ let xxd = (new XXD(contents));
+ xxd.exec();
+ system.print(xxd.output);
+ });
+ }
+ });
+} ()
diff --git a/src/SystemA.src b/src/SystemA.src
new file mode 100644
index 0000000..fbc1b58
--- /dev/null
+++ b/src/SystemA.src
@@ -0,0 +1,22 @@
+export
+class SystemA {
+ run() {
+ throw new Error("unimplemented!");
+ }
+
+ read(path) {
+ throw new Error("unimplemented!");
+ }
+
+ print(bytes) {
+ throw new Error("unimplemented!");
+ }
+
+ static init($kind) {
+ try {
+ return new $kind;
+ } catch (ex) {
+ return null;
+ }
+ }
+}
diff --git a/src/SystemB.src b/src/SystemB.src
new file mode 100644
index 0000000..7bc766d
--- /dev/null
+++ b/src/SystemB.src
@@ -0,0 +1,79 @@
+import { SystemA } from "./SystemA.src"
+
+export
+class SystemB extends SystemA {
+ constructor() {
+ super();
+
+ if (typeof(window) == "undefined") {
+ throw new Error("Not a browser environment");
+ }
+
+ this._page = window.document.body;
+
+ this._app = null;
+ this._file = null;
+ this._fileInput = null;
+ }
+
+ setUpForExec(app) {
+ this._app = app;
+
+ this._page.onload = (() => {
+ this._page.innerHTML = "";
+
+ this._page.style.fontFamily = "monospace";
+ this._page.style.whiteSpace = "pre-wrap";
+
+ let doc = this._page.ownerDocument;
+ let $$ = doc.createElement.bind(doc);
+
+ let button = $$("input");
+ button.type = "button";
+ button.value = "Load\u2026";
+ button.style.float = "right";
+ button.onclick = () => void(this._fileInput.click());
+ this._page.appendChild(button);
+
+ let fileInput = $$("input");
+ fileInput.type = "file";
+ fileInput.style.display = "none";
+ fileInput.onchange = this._onFileSelected.bind(this);
+ this._fileInput = this._page.appendChild(fileInput);
+
+ this._page.appendChild($$("div")).id = "container";
+ })
+ }
+
+ _onFileSelected(event) {
+ this._file = event.target.files[0];
+ let path = this._file.webkitRelativePath;
+ this._app.run(this, path);
+ }
+
+ read(path) {
+ return new Promise((resolve, reject) => {
+ if (path == this._file.webkitRelativePath) {
+ if (typeof(FileReader) != "undefined") {
+ let reader = new FileReader();
+
+ reader.addEventListener("loadend", (event) => {
+ resolve(new Uint8Array(event.target.result));
+ });
+
+ return void(reader.readAsArrayBuffer(this._file));
+ }
+ return reject(Error("FileReader API not implemented"));
+ }
+ return reject(Error("Cannot read file with path: " + path));
+ });
+ }
+
+ print(output) {
+ var message = "";
+ for (let i = 0, n = output.length; i < n; i++) {
+ message += String.fromCharCode(output[i]);
+ }
+ this._page.textContent = message;
+ }
+}
diff --git a/src/SystemC.src b/src/SystemC.src
new file mode 100644
index 0000000..4161f4d
--- /dev/null
+++ b/src/SystemC.src
@@ -0,0 +1,75 @@
+import { SystemA } from "./SystemA.src"
+
+export
+class SystemC extends SystemA {
+ constructor() {
+ super();
+
+ if (typeof(process) == "undefined") {
+ throw new Error("Not a NodeJS environment");
+ }
+
+ let [ engine, script, ...args ] = process.argv;
+ this.args = args;
+
+ this._nodeFS = process.mainModule.require("fs");
+ this._nodePath = process.mainModule.require("path");
+
+ const { FileNodeFSAdapter } = SystemC;
+
+ this._files = new FileNodeFSAdapter(this._nodeFS, this._nodePath);
+ }
+
+ setUpForExec(app) {
+ if (this.args.length == 1) {
+ return void(app.run(this, this.args[0]));
+ }
+ throw new Error("Expected file path");
+ }
+
+ read(path) {
+ return this._files.read(path, true);
+ }
+
+ print(output) {
+ var message = "";
+ for (let i = 0, n = output.length; i < n; i++) {
+ message += String.fromCharCode(output[i]);
+ }
+
+ if (typeof(console) != "undefined") {
+ console.log(message);
+ } else { // should not happen, but the typeof check is unavoidable
+ throw new Error("Cannot print message " + JSON.stringify(message));
+ }
+ }
+}
+
+SystemC.FileNodeFSAdapter = class FileNodeFSAdapter {
+ constructor(fs, path, workDir = ".") {
+ this._nodeFS = fs;
+ this._nodePath = path;
+ this._workDir = workDir;
+ }
+
+ read(path, raw) {
+ if (!path.startsWith("/")) {
+ path = this._nodePath.join(this._workDir, path);
+ }
+ if (!raw) {
+ var options = { encoding: "utf8" };
+ }
+
+ return new Promise((resolve, reject) => {
+ void this._nodeFS.readFile(path, options, (err, contents) => {
+ if (err) {
+ reject(err);
+ } else if (!raw) {
+ resolve(contents);
+ } else {
+ resolve(Uint8Array.from(contents));
+ }
+ });
+ });
+ }
+}