starting on js scripting language; worker msgs can run async functions (but we don't need to ... yet)

This commit is contained in:
Steven Hugg 2021-08-12 18:19:39 -05:00
parent bd00d98b77
commit a8b2b7c043
17 changed files with 1025 additions and 42 deletions

View File

@ -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;
}

164
package-lock.json generated
View File

@ -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=="
}
}
}
}
}

View File

@ -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"

View File

@ -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);

159
src/common/script/bitmap.ts Normal file
View File

@ -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; i<pal.colors.length; i++) {
pal.colors[i] = 0xff000000 | (i * 7919);
}
return pal;
}
export function rgbaToUint32(rgba: number[]): number {
let v = 0;
v |= rgba[0] << 0;
v |= rgba[1] << 8;
v |= rgba[2] << 16;
v |= rgba[3] << 24;
return v;
}
export function rgba(width: number, height: number) {
return new RGBABitmap(width, height);
}
export function indexed(width: number, height: number, bpp: number) {
return new IndexedBitmap(width, height, bpp);
}
export type BitmapType = RGBABitmap | IndexedBitmap;
export namespace png {
export function load(url: string): BitmapType {
return decode(io.loadbin(url));
}
export function decode(data: Uint8Array): BitmapType {
let png = fastpng.decode(data);
return convertToBitmap(png);
}
function convertToBitmap(png: fastpng.IDecodedPNG): BitmapType {
if (png.palette && png.depth <= 8) {
return convertIndexedToBitmap(png);
} else {
return convertRGBAToBitmap(png);
}
}
function convertIndexedToBitmap(png: fastpng.IDecodedPNG): IndexedBitmap {
var palarr = <any>png.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));
}
}

125
src/common/script/env.ts Normal file
View File

@ -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<void> {
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,
}];
}
}

69
src/common/script/io.ts Normal file
View File

@ -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;
}
}

165
src/common/script/node.ts Normal file
View File

@ -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<void> = 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<void> {
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<void> {
// 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<void>;
}
class ValueNode<T> 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<T> extends ValueNode<T> {
}
class IntegerNode extends ValueNode<number> {
}
abstract class BitmapNode extends ComputeNode {
width: number;
height: number;
}
class RGBABitmapNode extends BitmapNode {
rgba: ArrayNode<Uint32Array>;
compute(): Promise<void> {
throw new Error("Method not implemented.");
}
}
class IndexedBitmapNode extends BitmapNode {
indices: ArrayNode<Uint8Array>;
compute(): Promise<void> {
throw new Error("Method not implemented.");
}
}
class PaletteNode {
colors: ArrayNode<Uint32Array>;
compute(): Promise<void> {
throw new Error("Method not implemented.");
}
}
class PaletteMapNode extends ComputeNode {
palette: PaletteNode;
indices: ArrayNode<Uint8Array>;
compute(): Promise<void> {
throw new Error("Method not implemented.");
}
}
function valueOf<T>(node : ValueNode<T>) : 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<number>(1234);
var arr1 = new ValueNode<number[]>([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();

View File

@ -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 <stdint.h>\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);
}

13
src/common/script/test.ts Normal file
View File

@ -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)

View File

@ -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, '&amp;').replace(/[<]/g, '&lt;').replace(/[>]/g, '&gt;');
}

View File

@ -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<arr.length; i++) {
var d = arr[i];
@ -266,7 +266,7 @@ export function getPaletteLength(palfmt: PixelEditorPaletteFormat) : number {
}
}
function convertPaletteFormat(palbytes:UintArray, palfmt: PixelEditorPaletteFormat) : number[] {
export function convertPaletteFormat(palbytes:UintArray, palfmt: PixelEditorPaletteFormat) : number[] {
var pal = palfmt.pal;
var newpalette;
if (typeof pal === 'number') {
@ -290,7 +290,7 @@ function convertPaletteFormat(palbytes:UintArray, palfmt: PixelEditorPaletteForm
}
// TODO: illegal colors?
var PREDEF_PALETTES = {
const PREDEF_PALETTES = {
'nes':[
0x525252, 0xB40000, 0xA00000, 0xB1003D, 0x740069, 0x00005B, 0x00005F, 0x001840, 0x002F10, 0x084A08, 0x006700, 0x124200, 0x6D2800, 0x000000, 0x000000, 0x000000,
0xC4D5E7, 0xFF4000, 0xDC0E22, 0xFF476B, 0xD7009F, 0x680AD7, 0x0019BC, 0x0054B1, 0x006A5B, 0x008C03, 0x00AB00, 0x2C8800, 0xA47200, 0x000000, 0x000000, 0x000000,

View File

@ -221,16 +221,20 @@ function getCurrentPresetTitle() : string {
return current_preset.title || current_preset.name || current_project.mainPath || "ROM";
}
async function initProject() {
async function newFilesystem() {
var basefs : ProjectFilesystem = new WebPresetsFileSystem(platform_id);
if (isElectron) {
console.log('using electron with local filesystem', alternateLocalFilesystem);
var filesystem = new OverlayFilesystem(basefs, alternateLocalFilesystem);
return new OverlayFilesystem(basefs, alternateLocalFilesystem);
} else if (qs.localfs != null) {
var filesystem = new OverlayFilesystem(basefs, await getLocalFilesystem(qs.localfs));
return new OverlayFilesystem(basefs, await getLocalFilesystem(qs.localfs));
} else {
var filesystem = new OverlayFilesystem(basefs, new LocalForageFilesystem(store));
return new OverlayFilesystem(basefs, new LocalForageFilesystem(store));
}
}
async function initProject() {
var filesystem = await newFilesystem();
current_project = new CodeProject(newWorker(), platform_id, platform, filesystem);
projectWindows = new ProjectWindows($("#workspace")[0] as HTMLElement, current_project);
current_project.callbackBuildResult = (result:WorkerResult) => {

View File

@ -17,6 +17,7 @@ export function importPlatform(name: string) : Promise<any> {
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");

164
src/platform/script.ts Normal file
View File

@ -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<T> {
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<number> {
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 = $(`<iframe sandbox="allow-same-origin" width="100%" height="100%"/>`).appendTo(mainElement)[0] as HTMLIFrameElement;
mainElement.classList.add("vertical-scroll"); //100% height
mainElement.style.overflowY = 'auto';
this.iframe.onload = (e) => {
let head = this.iframe.contentDocument.head;
head.appendChild($(`<link rel="stylesheet" href="css/script.css">`)[0]);
};
*/
this.notebook = new Notebook(document, mainElement);
}
start() {
}
reset() {
}
pause() {
}
resume() {
}
loadROM(title, cells: Cell[]) {
this.notebook.updateCells(cells);
}
isRunning() {
return false;
}
isDebugging(): boolean {
return false;
}
getToolForFilename(fn: string): string {
return "js";
}
getDefaultExtension(): string {
return ".js";
}
getPresets() {
return [
];
}
/*
showHelp() {
window.open("https://github.com/showdownjs/showdown/wiki/Showdown's-Markdown-syntax", "_help");
}
*/
}
PLATFORMS['script'] = ScriptingPlatform;

View File

@ -0,0 +1,29 @@
import { Environment } from "../../common/script/env";
import { BuildStep, BuildStepResult, emglobal, store } from "../workermain";
// cache environments
var environments : {[path:string] : Environment} = {};
function getEnv(path: string) : Environment {
var env = environments[path];
if (!env) {
env = environments[path] = new Environment(emglobal, path);
// TODO: load environment from store?
}
return env;
}
export async function runJavascript(step: BuildStep) : Promise<BuildStepResult> {
var env = getEnv(step.path);
var code = store.getFileAsString(step.path);
try {
await env.run(code);
} catch (e) {
return {errors: env.extractErrors(e)};
}
var output = env.render();
return {
output
};
}

View File

@ -394,7 +394,7 @@ export interface BuildStep extends WorkerBuildStep {
///
class FileWorkingStore {
export class FileWorkingStore {
workfs : {[path:string]:FileEntry} = {};
workerseq : number = 0;
@ -452,7 +452,7 @@ class Builder {
wasChanged(entry:FileEntry) : boolean {
return entry.ts > this.startseq;
}
executeBuildSteps() {
async executeBuildSteps() : Promise<WorkerResult> {
this.startseq = store.currentVersion();
var linkstep : BuildStep = null;
while (this.steps.length) {
@ -462,7 +462,7 @@ class Builder {
if (!toolfn) throw Error("no tool named " + step.tool);
step.params = PLATFORM_PARAMS[getBasePlatform(platform)];
try {
step.result = toolfn(step);
step.result = await toolfn(step);
} catch (e) {
console.log("EXCEPTION", e, e.stack);
return {errors:[{line:0, msg:e+""}]}; // TODO: catch errors already generated?
@ -509,7 +509,7 @@ class Builder {
}
}
}
handleMessage(data: WorkerMessage) : WorkerResult {
async handleMessage(data: WorkerMessage) : Promise<WorkerResult> {
this.steps = [];
// file updates
if (data.updates) {
@ -528,7 +528,7 @@ class Builder {
}
// execute build steps
if (this.steps.length) {
var result = this.executeBuildSteps();
var result = await this.executeBuildSteps();
return result ? result : {unchanged:true};
}
// TODO: cache results
@ -1029,6 +1029,7 @@ import * as m6502 from './tools/m6502'
import * as z80 from './tools/z80'
import * as x86 from './tools/x86'
import * as arm from './tools/arm'
import * as script from './tools/script'
var TOOLS = {
'dasm': dasm.assembleDASM,
@ -1064,6 +1065,7 @@ var TOOLS = {
'wiz': misc.compileWiz,
'armips': arm.assembleARMIPS,
'vasmarm': arm.assembleVASMARM,
'js': script.runJavascript,
}
var TOOL_PRELOADFS = {
@ -1092,7 +1094,9 @@ var TOOL_PRELOADFS = {
'wiz': 'wiz',
}
function handleMessage(data : WorkerMessage) : WorkerResult {
const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay));
async function handleMessage(data : WorkerMessage) : Promise<WorkerResult> {
// preload file system
if (data.preload) {
var fs = TOOL_PRELOADFS[data.preload];
@ -1113,12 +1117,15 @@ function handleMessage(data : WorkerMessage) : WorkerResult {
}
if (ENVIRONMENT_IS_WORKER) {
onmessage = function(e) {
var result = handleMessage(e.data);
var lastpromise = null;
onmessage = async function(e) {
await lastpromise; // wait for previous message to complete
lastpromise = handleMessage(e.data);
var result = await lastpromise;
lastpromise = null;
if (result) {
postMessage(result);
}
}
}
//}();