From a8b2b7c0430bf628d24d7e6d28a45b3c1c4ad028 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Thu, 12 Aug 2021 18:19:39 -0500 Subject: [PATCH] starting on js scripting language; worker msgs can run async functions (but we don't need to ... yet) --- css/ui.css | 25 ++++++ package-lock.json | 164 +++++++++++++++++++++++++++++++---- package.json | 6 +- src/common/emu.ts | 8 +- src/common/script/bitmap.ts | 159 ++++++++++++++++++++++++++++++++++ src/common/script/env.ts | 125 +++++++++++++++++++++++++++ src/common/script/io.ts | 69 +++++++++++++++ src/common/script/node.ts | 165 ++++++++++++++++++++++++++++++++++++ src/common/script/output.ts | 85 +++++++++++++++++++ src/common/script/test.ts | 13 +++ src/common/util.ts | 7 +- src/ide/pixeleditor.ts | 10 +-- src/ide/ui.ts | 12 ++- src/platform/_index.ts | 1 + src/platform/script.ts | 164 +++++++++++++++++++++++++++++++++++ src/worker/tools/script.ts | 29 +++++++ src/worker/workermain.ts | 25 ++++-- 17 files changed, 1025 insertions(+), 42 deletions(-) create mode 100644 src/common/script/bitmap.ts create mode 100644 src/common/script/env.ts create mode 100644 src/common/script/io.ts create mode 100644 src/common/script/node.ts create mode 100644 src/common/script/output.ts create mode 100644 src/common/script/test.ts create mode 100644 src/platform/script.ts create mode 100644 src/worker/tools/script.ts diff --git a/css/ui.css b/css/ui.css index 07a9eca9..c4cc6d68 100644 --- a/css/ui.css +++ b/css/ui.css @@ -768,3 +768,28 @@ div.asset_toolbar { .waverow.editable:hover { background-color: #336633; } + +.scripting-cell { + background: #444; + color: #99cc99; + padding: 0.5em; + margin: 0.5em; + font-family: "Andale Mono", "Menlo", "Lucida Console", monospace; + word-wrap: break-word; +} +.scripting-cell canvas { + height: 15vw; + border: 2px solid #222; + outline-color: #ccc; + background: #000; + padding: 6px; + margin: 6px; + pointer-events:auto; +} +.scripting-cell canvas:focus { + outline:none; + border-color:#888; +} +.scripting-cell div { + display: inline; +} diff --git a/package-lock.json b/package-lock.json index aef0fb4b..2934eb50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,13 +13,16 @@ "@wasmer/wasmfs": "^0.12.0", "binaryen": "^101.0.0", "clipboard": "^2.0.6", + "error-stack-parser": "^2.0.6", + "fast-png": "^5.0.4", "file-saver": "^2.0.5", "jquery": "^3.6.0", "jszip": "^3.7.0", "localforage": "^1.9.0", "mousetrap": "^1.6.5", "octokat": "^0.10.0", - "split.js": "^1.6.2" + "split.js": "^1.6.2", + "yufka": "^2.0.1" }, "devDependencies": { "@types/bootbox": "^5.1.3", @@ -40,7 +43,6 @@ "lzg": "^1.0.x", "mocha": "^7.2.0", "mocha-simple-html-reporter": "^2.0.0", - "pngjs": "^3.4.0", "typedoc": "^0.21.0", "typescript": "^4.3.4", "typescript-formatter": "^7.2.2" @@ -820,6 +822,11 @@ "integrity": "sha512-CMjgRNsks27IDwI785YMY0KLt3co/c0cQ5foxHYv/shC2w8oOnVwz5Ubq1QG5KzrcW+AXk6gzdnxIkDnTvzu3g==", "devOptional": true }, + "node_modules/@types/pako": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-1.0.2.tgz", + "integrity": "sha512-8UJl2MjkqqS6ncpLZqRZ5LmGiFBkbYxocD4e4jmBqGvfRG1RS23gKsBQbdtV9O9GvRyjFTiRHRByjSlKCLlmZw==" + }, "node_modules/@types/plist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.2.tgz", @@ -3180,6 +3187,14 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/error-stack-parser": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.6.tgz", + "integrity": "sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==", + "dependencies": { + "stackframe": "^1.1.1" + } + }, "node_modules/es-abstract": { "version": "1.18.5", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz", @@ -3419,6 +3434,21 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "devOptional": true }, + "node_modules/fast-png": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-5.0.4.tgz", + "integrity": "sha512-vTNj6yixRnclW6sTlCeH6sNRLBOhM5ITmlo1LSU5ojKEc2e9kZkqXPo2xzBxKb61MBCXRXBcr8qJztOHr2O6WQ==", + "dependencies": { + "@types/pako": "^1.0.1", + "iobuffer": "^5.0.2", + "pako": "^2.0.2" + } + }, + "node_modules/fast-png/node_modules/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==" + }, "node_modules/fastq": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.1.tgz", @@ -4503,6 +4533,11 @@ "integrity": "sha512-j8grHGDzv1v+8T1sAQ+3boTCntFPfvxLCkNcxB1J8qA0lUN+fAlSyYd+RXKvaPRL4AGyPxViutBEJHNXOyUdFQ==", "optional": true }, + "node_modules/iobuffer": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.0.3.tgz", + "integrity": "sha512-0SNk4hbHVXx9oE27vTJY+oiI0txkhBdQV12RvILd/7XuIhBZ0TkImq5EnhFYCcRcDff8jpFhZ9C2Sg+NIo3ZMQ==" + }, "node_modules/ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -5890,6 +5925,14 @@ "node": ">=0.8.0" } }, + "node_modules/magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dependencies": { + "sourcemap-codec": "^1.4.4" + } + }, "node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -7699,15 +7742,6 @@ "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", "dev": true }, - "node_modules/pngjs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", - "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", - "dev": true, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -8614,6 +8648,11 @@ "source-map": "^0.6.0" } }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + }, "node_modules/spawn-wrap": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.3.tgz", @@ -8720,6 +8759,11 @@ "node": ">=0.10.0" } }, + "node_modules/stackframe": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz", + "integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==" + }, "node_modules/stat-mode": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", @@ -10061,6 +10105,29 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } + }, + "node_modules/yufka": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/yufka/-/yufka-2.0.1.tgz", + "integrity": "sha512-VdqiJocmYfhx/yPiLFmF6ZyxKE2bzaHbocO0Y27sC5ytSQ09CLhTWP8GJAZMhGq2UGvhCM6wYXuDxNP5PUusHg==", + "dependencies": { + "acorn": "^8.0.1", + "magic-string": "^0.25.2" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/yufka/node_modules/acorn": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", + "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } } }, "dependencies": { @@ -10703,6 +10770,11 @@ "integrity": "sha512-CMjgRNsks27IDwI785YMY0KLt3co/c0cQ5foxHYv/shC2w8oOnVwz5Ubq1QG5KzrcW+AXk6gzdnxIkDnTvzu3g==", "devOptional": true }, + "@types/pako": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-1.0.2.tgz", + "integrity": "sha512-8UJl2MjkqqS6ncpLZqRZ5LmGiFBkbYxocD4e4jmBqGvfRG1RS23gKsBQbdtV9O9GvRyjFTiRHRByjSlKCLlmZw==" + }, "@types/plist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.2.tgz", @@ -12588,6 +12660,14 @@ "is-arrayish": "^0.2.1" } }, + "error-stack-parser": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.6.tgz", + "integrity": "sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==", + "requires": { + "stackframe": "^1.1.1" + } + }, "es-abstract": { "version": "1.18.5", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz", @@ -12759,6 +12839,23 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "devOptional": true }, + "fast-png": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-5.0.4.tgz", + "integrity": "sha512-vTNj6yixRnclW6sTlCeH6sNRLBOhM5ITmlo1LSU5ojKEc2e9kZkqXPo2xzBxKb61MBCXRXBcr8qJztOHr2O6WQ==", + "requires": { + "@types/pako": "^1.0.1", + "iobuffer": "^5.0.2", + "pako": "^2.0.2" + }, + "dependencies": { + "pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==" + } + } + }, "fastq": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.1.tgz", @@ -13618,6 +13715,11 @@ "integrity": "sha512-j8grHGDzv1v+8T1sAQ+3boTCntFPfvxLCkNcxB1J8qA0lUN+fAlSyYd+RXKvaPRL4AGyPxViutBEJHNXOyUdFQ==", "optional": true }, + "iobuffer": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.0.3.tgz", + "integrity": "sha512-0SNk4hbHVXx9oE27vTJY+oiI0txkhBdQV12RvILd/7XuIhBZ0TkImq5EnhFYCcRcDff8jpFhZ9C2Sg+NIo3ZMQ==" + }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -14719,6 +14821,14 @@ } } }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -16171,12 +16281,6 @@ "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", "dev": true }, - "pngjs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", - "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", - "dev": true - }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -16879,6 +16983,11 @@ "source-map": "^0.6.0" } }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + }, "spawn-wrap": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.3.tgz", @@ -16973,6 +17082,11 @@ "tweetnacl": "~0.14.0" } }, + "stackframe": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz", + "integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==" + }, "stat-mode": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", @@ -18055,6 +18169,22 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } + }, + "yufka": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/yufka/-/yufka-2.0.1.tgz", + "integrity": "sha512-VdqiJocmYfhx/yPiLFmF6ZyxKE2bzaHbocO0Y27sC5ytSQ09CLhTWP8GJAZMhGq2UGvhCM6wYXuDxNP5PUusHg==", + "requires": { + "acorn": "^8.0.1", + "magic-string": "^0.25.2" + }, + "dependencies": { + "acorn": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", + "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==" + } + } } } } diff --git a/package.json b/package.json index 21e7e210..f66a641a 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,16 @@ "@wasmer/wasmfs": "^0.12.0", "binaryen": "^101.0.0", "clipboard": "^2.0.6", + "error-stack-parser": "^2.0.6", + "fast-png": "^5.0.4", "file-saver": "^2.0.5", "jquery": "^3.6.0", "jszip": "^3.7.0", "localforage": "^1.9.0", "mousetrap": "^1.6.5", "octokat": "^0.10.0", - "split.js": "^1.6.2" + "split.js": "^1.6.2", + "yufka": "^2.0.1" }, "devDependencies": { "@types/bootbox": "^5.1.3", @@ -42,7 +45,6 @@ "lzg": "^1.0.x", "mocha": "^7.2.0", "mocha-simple-html-reporter": "^2.0.0", - "pngjs": "^3.4.0", "typedoc": "^0.21.0", "typescript": "^4.3.4", "typescript-formatter": "^7.2.2" diff --git a/src/common/emu.ts b/src/common/emu.ts index c0b16051..9468ea09 100644 --- a/src/common/emu.ts +++ b/src/common/emu.ts @@ -31,8 +31,8 @@ export function setNoiseSeed(x : number) { type KeyboardCallback = (which:number, charCode:number, flags:KeyFlags) => void; -function __createCanvas(mainElement:HTMLElement, width:number, height:number) : HTMLCanvasElement { - var canvas = document.createElement('canvas'); +function __createCanvas(doc:HTMLDocument, mainElement:HTMLElement, width:number, height:number) : HTMLCanvasElement { + var canvas = doc.createElement('canvas'); canvas.width = width; canvas.height = height; canvas.classList.add("emuvideo"); @@ -104,9 +104,9 @@ export class RasterVideo { } } - create() { + create(doc?: HTMLDocument) { var canvas; - this.canvas = canvas = __createCanvas(this.mainElement, this.width, this.height); + this.canvas = canvas = __createCanvas(doc || document, this.mainElement, this.width, this.height); this.vcanvas = $(canvas); if (this.options && this.options.rotate) { this.setRotate(this.options.rotate); diff --git a/src/common/script/bitmap.ts b/src/common/script/bitmap.ts new file mode 100644 index 00000000..e8a1a0ef --- /dev/null +++ b/src/common/script/bitmap.ts @@ -0,0 +1,159 @@ + +import * as fastpng from 'fast-png'; +import { convertWordsToImages, PixelEditorImageFormat } from '../../ide/pixeleditor'; +import { arrayCompare } from '../util'; +import * as io from './io' + +export abstract class AbstractBitmap { + constructor( + public readonly width: number, + public readonly height: number, + ) { + } +} + +export class RGBABitmap extends AbstractBitmap { + public readonly rgba: Uint32Array + + constructor( + width: number, + height: number, + ) { + super(width, height); + this.rgba = new Uint32Array(this.width * this.height); + } + setPixel(x: number, y: number, rgba: number): void { + this.rgba[y * this.width + x] = rgba; + } + getPixel(x: number, y: number): number { + return this.rgba[y * this.width + x]; + } +} + +export abstract class MappedBitmap extends AbstractBitmap { + public readonly pixels: Uint8Array + + constructor( + width: number, + height: number, + public readonly bitsPerPixel: number, + pixels?: Uint8Array + ) { + super(width, height); + this.pixels = pixels || new Uint8Array(this.width * this.height); + } + + setPixel(x: number, y: number, index: number): void { + this.pixels[y * this.width + x] = index; + } + getPixel(x: number, y: number): number { + return this.pixels[y * this.width + x]; + } + abstract getRGBAForIndex(index: number): number; +} + +export class Palette { + colors: Uint32Array; + + constructor(numColors: number) { + this.colors = new Uint32Array(numColors); + } +} + +export class IndexedBitmap extends MappedBitmap { + public palette: Palette; + + constructor( + width: number, + height: number, + bitsPerPixel: number, + pixels?: Uint8Array + ) { + super(width, height, bitsPerPixel, pixels); + this.palette = getDefaultPalette(bitsPerPixel); + } + + getRGBAForIndex(index: number): number { + return this.palette.colors[index]; + } +} + +// TODO? +function getDefaultPalette(bpp: number) { + var pal = new Palette(1 << bpp); + for (var i=0; ipng.palette as [number, number, number, number][]; + var palette = new Palette(palarr.length); + for (let i = 0; i < palarr.length; i++) { + // TODO: alpha? + palette.colors[i] = rgbaToUint32(palarr[i]) | 0xff000000; + } + let bitmap = new IndexedBitmap(png.width, png.height, png.depth); + for (let i = 0; i < bitmap.pixels.length; i++) { + bitmap.pixels[i] = png.data[i]; + } + bitmap.palette = palette; + return bitmap; + } + function convertRGBAToBitmap(png: fastpng.IDecodedPNG): RGBABitmap { + let bitmap = new RGBABitmap(png.width, png.height); + let rgba = [0, 0, 0, 0]; + for (let i = 0; i < bitmap.rgba.length; i++) { + for (let j = 0; j < 4; j++) + rgba[j] = png.data[i * 4 + j]; + bitmap.rgba[i] = rgbaToUint32(rgba); + } + return bitmap; + } +} + +export namespace from { + // TODO: check arguments + export function bytes(arr: Uint8Array, fmt: PixelEditorImageFormat) { + var pixels = convertWordsToImages(arr, fmt); + // TODO: guess if missing w/h/count? + // TODO: reverse mapping + // TODO: maybe better composable functions + return pixels.map(data => new IndexedBitmap(fmt.w, fmt.h, fmt.bpp|1, data)); + } +} diff --git a/src/common/script/env.ts b/src/common/script/env.ts new file mode 100644 index 00000000..dcad95f8 --- /dev/null +++ b/src/common/script/env.ts @@ -0,0 +1,125 @@ +import { WorkerError } from "../workertypes"; +import ErrorStackParser = require("error-stack-parser"); +import yufka from 'yufka'; +import * as bitmap from "./bitmap"; +import * as io from "./io"; +import * as output from "./output"; +import { escapeHTML } from "../util"; + +export interface Cell { + id: string; + object?: any; +} + +const IMPORTS = { + 'bitmap': bitmap, + 'io': io, + 'output': output +} + +const LINE_NUMBER_OFFSET = 3; + +const GLOBAL_BADLIST = [ + 'eval' +] + +const GLOBAL_GOODLIST = [ + 'eval', // 'eval' can't be defined or assigned to in strict mode code + 'Math', 'JSON', + 'parseFloat', 'parseInt', 'isFinite', 'isNaN', + 'String', 'Symbol', 'Number', 'Object', 'Boolean', 'NaN', 'Infinity', 'Date', 'BigInt', + 'Set', 'Map', 'RegExp', 'Array', 'ArrayBuffer', 'DataView', + 'Float32Array', 'Float64Array', + 'Int8Array', 'Int16Array', 'Int32Array', + 'Uint8Array', 'Uint16Array', 'Uint32Array', 'Uint8ClampedArray', +] + +export class Environment { + preamble: string; + postamble: string; + obj: {}; + + constructor( + public readonly globalenv: any, + public readonly path: string + ) { + var badlst = Object.getOwnPropertyNames(this.globalenv).filter(name => GLOBAL_GOODLIST.indexOf(name) < 0); + this.preamble = `'use strict';var ${badlst.join(',')};`; + for (var impname in IMPORTS) { + this.preamble += `var ${impname}=$$.${impname};` + } + this.preamble += '{\n'; + this.postamble = '\n}'; + } + preprocess(code: string): string { + var declvars = {}; + const result = yufka(code, (node, { update, source, parent }) => { + switch (node.type) { + case 'Identifier': + if (GLOBAL_BADLIST.indexOf(source()) >= 0) { + update(`__FORBIDDEN__KEYWORD__${source()}__`) // TODO? how to preserve line number? + } + break; + case 'AssignmentExpression': + /* + // x = expr --> var x = expr + if (parent().type === 'ExpressionStatement' && parent(2) && parent(2).type === 'Program') { // TODO + let left = node['left']; + if (left && left.type === 'Identifier' && declvars[left.name] == null) { + update(`var ${source()}`) + declvars[left.name] = true; + } + } + */ + break; + } + }) + return result.toString(); + } + async run(code: string): Promise { + code = this.preprocess(code); + this.obj = {}; + const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor; + const fn = new AsyncFunction('$$', this.preamble + code + this.postamble).bind(this.obj, IMPORTS); + await fn.call(this); + this.checkResult(); + } + checkResult() { + for (var [key, value] of Object.entries(this.obj)) { + if (value instanceof Promise) { + throw new Error(`'${key}' is unresolved. Use 'await' before expression.`) // TODO? + } + } + } + render(): Cell[] { + var cells = []; + for (var [key, value] of Object.entries(this.obj)) { + if (typeof value === 'function') { + // TODO: find other values, functions embedded in objects? + } else { + var cell: Cell = { id: key, object: value }; + cells.push(cell); + } + } + return cells; + } + extractErrors(e: Error): WorkerError[] { + if (e['loc'] != null) { + return [{ + path: this.path, + msg: e.message, + line: e['loc'].line, + start: e['loc'].column, + }] + } + // TODO: Cannot parse given Error object + var frames = ErrorStackParser.parse(e); + var frame = frames.find(f => f.functionName === 'anonymous'); + return [{ + path: this.path, + msg: e.message, + line: frame ? frame.lineNumber - LINE_NUMBER_OFFSET : 0, + start: frame ? frame.columnNumber : 0, + }]; + } +} diff --git a/src/common/script/io.ts b/src/common/script/io.ts new file mode 100644 index 00000000..0f0df744 --- /dev/null +++ b/src/common/script/io.ts @@ -0,0 +1,69 @@ +import { ProjectFilesystem } from "../../ide/project"; +import { FileData } from "../workertypes"; + +// TODO + +var $$fs: ProjectFilesystem; +var $$cache: { [path: string]: FileData } = {}; + +export class IOWaitError extends Error { +} + +export function $$setupFS(fs: ProjectFilesystem) { + $$fs = fs; +} + +function getFS(): ProjectFilesystem { + if ($$fs == null) throw new Error(`Internal Error: The 'io' module has not been set up properly.`) + return $$fs; +} + +export function canonicalurl(url: string) : string { + // get raw resource URL for github + if (url.startsWith('https://github.com/')) { + let toks = url.split('/'); + if (toks[5] === 'blob') { + return `https://raw.githubusercontent.com/${toks[3]}/${toks[4]}/${toks.slice(6).join('/')}` + } + } + return url; +} + +export function load(url: string, type?: 'binary' | 'text'): FileData { + url = canonicalurl(url); + // TODO: only works in web worker + var xhr = new XMLHttpRequest(); + xhr.responseType = type === 'text' ? 'text' : 'arraybuffer'; + xhr.open("GET", url, false); // synchronous request + xhr.send(null); + if (xhr.response != null && xhr.status == 200) { + if (type !== 'text') { + return new Uint8Array(xhr.response); + } else { + return xhr.response; + } + } else { + throw new Error(`The resource at "${url}" responded with status code of ${xhr.status}.`) + } +} + +export function loadbin(url: string): Uint8Array { + var data = load(url, 'binary'); + if (data instanceof Uint8Array) + return data; + else + throw new Error(`The resource at "${url}" is not a binary file.`); +} + +export function xload(path: string): FileData { + var data = $$cache[path]; + if (data == null) { + getFS().getFileData(path).then((value) => { + $$cache[path] = value; + }) + throw new IOWaitError(path); + } else { + return data; + } +} + diff --git a/src/common/script/node.ts b/src/common/script/node.ts new file mode 100644 index 00000000..ccf8502c --- /dev/null +++ b/src/common/script/node.ts @@ -0,0 +1,165 @@ + +var lastTimestamp = 0; + +function newTimestamp() { + return ++lastTimestamp; +} + +export type DependencySet = {[id:string] : ComputeNode | any} + +export abstract class ComputeNode { + private src_ts: number = newTimestamp(); + private result_ts: number = 0; + private depends: DependencySet = {}; + private busy: Promise = null; + + modified() { + this.src_ts = newTimestamp(); + } + + isStale(ts: number) { + return this.result_ts < ts; + } + + setDependencies(depends: DependencySet) { + this.depends = depends; + // compute latest timestamp of all dependencies + var ts = 0; + for (let [key, dep] of Object.entries(this.depends)) { + if (dep instanceof ComputeNode && dep.result_ts) { + ts = Math.max(ts, dep.result_ts); + } else { + ts = newTimestamp(); + } + } + this.src_ts = ts; + } + + getDependencies() { + return this.depends; + } + + async update() : Promise { + let maxts = 0; + let dependsComputes = [] + for (let [key, dep] of Object.entries(this.depends)) { + if (dep instanceof ComputeNode && dep.isStale(this.src_ts)) { + dependsComputes.push(dep.compute()); + } + } + if (dependsComputes.length) { + await Promise.all(dependsComputes); + this.recompute(maxts); + } + } + + async recompute(ts: number) : Promise { + // are we currently waiting for a computation to finish? + if (this.busy == null || ts > this.result_ts) { + // wait for previous operation to finish (no-op if null) + await this.busy; + this.result_ts = ts; + this.busy = this.compute(); + } + await this.busy; + this.busy = null; + } + + abstract compute(): Promise; +} + +class ValueNode extends ComputeNode { + private value : T; + + constructor(value : T) { + super(); + this.set(value); + } + + get() : T { + return this.value; + } + + set(newValue : T) { + this.value = newValue; + this.modified(); + } + + async compute() { } +} + +class ArrayNode extends ValueNode { +} + +class IntegerNode extends ValueNode { +} + +abstract class BitmapNode extends ComputeNode { + width: number; + height: number; +} + +class RGBABitmapNode extends BitmapNode { + rgba: ArrayNode; + + compute(): Promise { + throw new Error("Method not implemented."); + } +} + +class IndexedBitmapNode extends BitmapNode { + indices: ArrayNode; + + compute(): Promise { + throw new Error("Method not implemented."); + } +} + +class PaletteNode { + colors: ArrayNode; + + compute(): Promise { + throw new Error("Method not implemented."); + } +} + +class PaletteMapNode extends ComputeNode { + palette: PaletteNode; + indices: ArrayNode; + + compute(): Promise { + throw new Error("Method not implemented."); + } +} + +function valueOf(node : ValueNode) : T { + return node.get(); +} + +class TestNode extends ComputeNode { + value : string; + + async compute() { + await new Promise(r => setTimeout(r, 100)); + this.value = Object.values(this.getDependencies()).map(valueOf).join(''); + } + +} + +/// + +async function test() { + var val1 = new ValueNode(1234); + var arr1 = new ValueNode([1,2,3]); + var join = new TestNode(); + join.setDependencies({a:val1, b:arr1}); + await join.update(); + console.log(join); + val1.set(9999); + join.update(); + val1.set(9989) + await join.update(); + console.log(join); +} + +test(); diff --git a/src/common/script/output.ts b/src/common/script/output.ts new file mode 100644 index 00000000..0d6bc7bb --- /dev/null +++ b/src/common/script/output.ts @@ -0,0 +1,85 @@ + +enum DataType { + unknown, + u8, + s8, + u16, + s16, + u32, + s32, + f32, + f64, +}; + +function getArrayDataType(value: any) : DataType { + if (value instanceof Uint8Array) { + return DataType.u8; + } else if (value instanceof Int8Array) { + return DataType.s8; + } else if (value instanceof Uint16Array) { + return DataType.u16; + } else if (value instanceof Int16Array) { + return DataType.s16; + } else if (value instanceof Uint32Array) { + return DataType.u32; + } else if (value instanceof Int32Array) { + return DataType.s32; + } else if (value instanceof Float32Array) { + return DataType.f32; + } else if (value instanceof Float64Array) { + return DataType.f64; + } +} + +export abstract class OutputFile { + constructor( + public readonly path : string, + public readonly decls : {} + ) { + } + abstract declToText(label: string, value: any) : string; + toString() : string { + return Object.entries(this.decls).map(entry => this.declToText(entry[0],entry[1])).join('\n\n'); + } +} + +export class COutputFile extends OutputFile { + toString() : string { + return `#include \n\n${super.toString()}\n`; + } + dataTypeToString(dtype: DataType) { + switch (dtype) { + case DataType.u8: return 'uint8_t'; + case DataType.s8: return 'int8_t'; + case DataType.u16: return 'uint16_t'; + case DataType.s16: return 'int16_t'; + case DataType.u32: return 'uint32_t'; + case DataType.s32: return 'int32_t'; + case DataType.f32: return 'float'; + case DataType.f64: return 'double'; + default: + throw new Error('Cannot convert data type'); // TODO + } + } + valueToString(value, atype: DataType) : string { + // TODO: round, check value + return value+""; + } + declToText(label: string, value: any) : string { + if (Array.isArray(value) || value['BYTES_PER_ELEMENT']) { + let atype = getArrayDataType(value); + if (atype != null) { + let dtypestr = this.dataTypeToString(atype); + let dtext = value.map(elem => this.valueToString(elem, atype)).join(','); + let len = value.length; + return `${dtypestr} ${label}[${len}] = { ${dtext} };`; + } + } + throw new Error(`Cannot convert array "${label}"`); // TODO + } +} + +// TODO: header file, auto-detect tool? +export function file(path: string, decls: {}) { + return new COutputFile(path, decls); +} diff --git a/src/common/script/test.ts b/src/common/script/test.ts new file mode 100644 index 00000000..7d518ec0 --- /dev/null +++ b/src/common/script/test.ts @@ -0,0 +1,13 @@ + +import 'fs'; +import * as bitmap from './bitmap' + +const fs = require('fs') + +var data = fs.readFileSync('images/book_a2600.png'); +//var data = fs.readFileSync('images/print-head.png'); +console.log(data); + +var png = bitmap.png.decode(data); +console.log(png) + diff --git a/src/common/util.ts b/src/common/util.ts index 680ac4de..9284d848 100644 --- a/src/common/util.ts +++ b/src/common/util.ts @@ -1,3 +1,4 @@ +import { UintArray } from "../ide/pixeleditor"; export function lpad(s:string, n:number):string { s += ''; // convert to string @@ -55,7 +56,7 @@ export function toradix(v:number, nd:number, radix:number) { } } -export function arrayCompare(a:any[], b:any[]):boolean { +export function arrayCompare(a:any[]|UintArray, b:any[]|UintArray):boolean { if (a == null && b == null) return true; if (a == null) return false; if (b == null) return false; @@ -618,3 +619,7 @@ export function parseXMLPoorly(s: string, openfn?: XMLVisitFunction, closefn?: X return top; } +export function escapeHTML(s: string): string { + return s.replace(/[&]/g, '&').replace(/[<]/g, '<').replace(/[>]/g, '>'); +} + diff --git a/src/ide/pixeleditor.ts b/src/ide/pixeleditor.ts index 805af0ff..0837c9c5 100644 --- a/src/ide/pixeleditor.ts +++ b/src/ide/pixeleditor.ts @@ -151,7 +151,7 @@ function reindexMask(x:number, inds:number[]) : [number, number] { return [i >> 3, i & 7]; } -function convertWordsToImages(words:UintArray, fmt:PixelEditorImageFormat) : Uint8Array[] { +export function convertWordsToImages(words:UintArray, fmt:PixelEditorImageFormat) : Uint8Array[] { var width = fmt.w; var height = fmt.h; var count = fmt.count || 1; @@ -190,7 +190,7 @@ function convertWordsToImages(words:UintArray, fmt:PixelEditorImageFormat) : Uin return images; } -function convertImagesToWords(images:Uint8Array[], fmt:PixelEditorImageFormat) : number[] { +export function convertImagesToWords(images:Uint8Array[], fmt:PixelEditorImageFormat) : number[] { if (fmt.destfmt) fmt = fmt.destfmt; var width = fmt.w; var height = fmt.h; @@ -236,7 +236,7 @@ function convertImagesToWords(images:Uint8Array[], fmt:PixelEditorImageFormat) : } // TODO -function convertPaletteBytes(arr:UintArray,r0,r1,g0,g1,b0,b1) : number[] { +export function convertPaletteBytes(arr:UintArray,r0,r1,g0,g1,b0,b1) : number[] { var result = []; for (var i=0; i { diff --git a/src/platform/_index.ts b/src/platform/_index.ts index c5ad53d2..9669305f 100644 --- a/src/platform/_index.ts +++ b/src/platform/_index.ts @@ -17,6 +17,7 @@ export function importPlatform(name: string) : Promise { case "msx": return import("../platform/msx"); case "mw8080bw": return import("../platform/mw8080bw"); case "nes": return import("../platform/nes"); + case "script": return import("../platform/script"); case "sms": return import("../platform/sms"); case "sound_konami": return import("../platform/sound_konami"); case "sound_williams": return import("../platform/sound_williams"); diff --git a/src/platform/script.ts b/src/platform/script.ts new file mode 100644 index 00000000..9461be34 --- /dev/null +++ b/src/platform/script.ts @@ -0,0 +1,164 @@ + +import { PLATFORMS, RasterVideo } from "../common/emu"; +import { Platform } from "../common/baseplatform"; +import { Cell } from "../common/script/env"; +import { escapeHTML } from "../common/util"; +import { BitmapType, IndexedBitmap, RGBABitmap } from "../common/script/bitmap"; + +abstract class TileEditor { + video: RasterVideo; + + constructor( + public readonly tileWidth: number, + public readonly tileHeight: number, + public readonly numColumns: number, + public readonly numRows: number, + ) { + } + getPixelWidth() { return this.tileWidth * this.numColumns } + getPixelHeight() { return this.tileHeight * this.numRows } + attach(div: HTMLElement) { + this.video = new RasterVideo(div, this.getPixelWidth(), this.getPixelHeight()); + this.video.create(); + } + detach() { + this.video = null; + } + update() { + if (this.video) this.video.updateFrame(); + } + abstract getTile(x: number, y: number): T; + abstract renderTile(x: number, y: number): void; +} + +class RGBABitmapEditor extends TileEditor { + constructor( + public readonly bitmap: RGBABitmap + ) { + super(1, 1, bitmap.width, bitmap.height); + } + getTile(x: number, y: number) { + return this.bitmap.getPixel(x, y); + } + renderTile(x: number, y: number): void { + //TODO + } +} + +function bitmap2image(doc: HTMLDocument, div: HTMLElement, bitmap: BitmapType): HTMLCanvasElement { + var video = new RasterVideo(div, bitmap.width, bitmap.height); + video.create(doc); + video.canvas.className = 'pixelated'; + let vdata = video.getFrameData(); + if (bitmap['palette'] != null) { + let bmp = bitmap as IndexedBitmap; + let pal = bmp.palette.colors; + for (var i = 0; i < bmp.pixels.length; i++) { + vdata[i] = pal[bmp.pixels[i]]; + } + } else { + let bmp = bitmap as RGBABitmap; + vdata.set(bmp.rgba); + } + video.updateFrame(); + return video.canvas; +} + +class Notebook { + constructor( + public readonly maindoc: HTMLDocument, + public readonly maindiv: HTMLElement + ) { + maindiv.classList.add('vertical-scroll'); + //maindiv.classList.add('container') + } + updateCells(cells: Cell[]) { + let body = this.maindiv; + body.innerHTML = ''; + //var body = $(this.iframe).contents().find('body'); + //body.empty(); + for (let cell of cells) { + if (cell.object != null) { + let div = this.objectToDiv(cell.object); + div.id = cell.id; + div.classList.add('scripting-cell') + //div.classList.add('row') + body.append(div); + } + } + } + objectToDiv(object: any) { + let div = document.createElement('div'); + //div.classList.add('col-auto') + //grid-template-columns: repeat(auto-fit, minmax(50px, 1fr)); + // TODO: tile editor + if (Array.isArray(object) || object.BYTES_PER_ELEMENT) { + object.forEach((obj) => { + div.appendChild(this.objectToDiv(obj)); + }); + // TODO + } else if (object['bitsPerPixel'] && object['pixels'] && object['palette']) { + bitmap2image(this.maindoc, div, object as IndexedBitmap); + } else if (object['rgba']) { + bitmap2image(this.maindoc, div, object as RGBABitmap); + } else if (object != null) { + div.innerHTML = escapeHTML(JSON.stringify(object)); + } + return div; + } +} + +class ScriptingPlatform implements Platform { + mainElement: HTMLElement; + iframe: HTMLIFrameElement; + notebook: Notebook; + + constructor(mainElement: HTMLElement) { + this.mainElement = mainElement; + /* + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe + this.iframe = $(`