From b1b832402359eeba455c7424e43ffdfdbfd1554b Mon Sep 17 00:00:00 2001 From: CanadaHonk Date: Wed, 25 Oct 2023 23:30:15 +0100 Subject: [PATCH] js: initial add!! --- engine/js/backends/kiesel.js | 74 ++++++++++++++++++++++++++++++ engine/js/backends/spidermonkey.js | 72 +++++++++++++++++++++++++++++ engine/js/index.js | 34 ++++++++++++++ engine/js/ipc/inside.js | 43 +++++++++++++++++ engine/js/ipc/outside.js | 34 ++++++++++++++ engine/layout.js | 10 ++++ engine/main.js | 31 +++++++++++-- engine/renderer.js | 9 ++++ 8 files changed, 304 insertions(+), 3 deletions(-) create mode 100644 engine/js/backends/kiesel.js create mode 100644 engine/js/backends/spidermonkey.js create mode 100644 engine/js/index.js create mode 100644 engine/js/ipc/inside.js create mode 100644 engine/js/ipc/outside.js diff --git a/engine/js/backends/kiesel.js b/engine/js/backends/kiesel.js new file mode 100644 index 0000000..c92fd52 --- /dev/null +++ b/engine/js/backends/kiesel.js @@ -0,0 +1,74 @@ +let wasmModule, WasmFs, WASI, browserBindings; +const loadWasm = async url => { + if (wasmModule) return wasmModule; + + // todo: use 1.x versions (breaking) + 0, { WASI } = await import('https://esm.sh/@wasmer/wasi@0.12.0'); + 0, browserBindings = (await import('https://esm.sh/@wasmer/wasi@0.12.0/lib/bindings/browser')).default; + + // const { WasmFs } = await import('https://esm.sh/@wasmer/wasmfs@0.12.0'); + 0, { WasmFs } = await import('https://esm.sh/@wasmer/wasmfs@0.12.0'); + // if (!wasmFs) wasmFs = new WasmFs(); + + const res = fetch(url); + return wasmModule = await WebAssembly.compileStreaming(res); +}; + +export const run = async (js, ipcHandler = () => {}, stdinCallback = () => {}) => { + // const wasmModule = await loadWasm('https://goose-cors.goosemod.workers.dev/?https://files.kiesel.dev/kiesel.wasm'); + const wasmModule = await loadWasm('https://files.kiesel.dev/kiesel.wasm'); + + const wasmFs = new WasmFs(); + + let wasi = new WASI({ + args: ['kiesel', '/input.js'], + preopens: {'/': '/'}, + env: {}, + bindings: { + ...browserBindings, + fs: wasmFs.fs, + }, + }); + + let instance = await WebAssembly.instantiate(wasmModule, wasi.getImports(wasmModule)); + + wasmFs.fs.writeFileSync('/input.js', js); + + wasmFs.volume.fds[1].position = 0; + wasmFs.volume.fds[2].position = 0; + + wasmFs.fs.writeFileSync('/dev/stdin', ""); + wasmFs.fs.writeFileSync('/dev/stdout', ""); + wasmFs.fs.writeFileSync('/dev/stderr', ""); + + stdinCallback(wasmFs.fs); + + let lastStdout = ''; + wasmFs.fs.watch('/dev/stdout', () => { + const stdout = wasmFs.fs.readFileSync('/dev/stdout', 'utf8'); + + const newStdout = stdout.slice(lastStdout.length); + + if (newStdout) { + const msgs = newStdout.split('\n'); + for (const x of msgs) { + if (x && x.endsWith('}')) { + ipcHandler(JSON.parse(x)); + lastStdout = stdout; + } + } + } + }); + + try { + wasi.start(instance); + } catch (e) { + console.error(e); + } + + console.log({ js }); + + /* let stdout = wasmFs.fs.readFileSync('/dev/stdout').toString(); + let stderr = wasmFs.fs.readFileSync('/dev/stderr').toString(); + console.log({stdout, stderr}); */ +}; \ No newline at end of file diff --git a/engine/js/backends/spidermonkey.js b/engine/js/backends/spidermonkey.js new file mode 100644 index 0000000..36c6576 --- /dev/null +++ b/engine/js/backends/spidermonkey.js @@ -0,0 +1,72 @@ +let wasmModule, WasmFs, WASI, browserBindings; +const loadWasm = async url => { + if (wasmModule) return wasmModule; + + // todo: use 1.x versions (breaking) + 0, { WASI } = await import('https://esm.sh/@wasmer/wasi@0.12.0'); + 0, browserBindings = (await import('https://esm.sh/@wasmer/wasi@0.12.0/lib/bindings/browser')).default; + + // const { WasmFs } = await import('https://esm.sh/@wasmer/wasmfs@0.12.0'); + 0, { WasmFs } = await import('https://esm.sh/@wasmer/wasmfs@0.12.0'); + // if (!wasmFs) wasmFs = new WasmFs(); + + const res = fetch(url); + return wasmModule = await WebAssembly.compileStreaming(res); +}; + +let data; +export const run = async (js, ipcHandler = () => {}, stdinCallback = () => {}) => { + if (!data) data = await (await fetch('https://mozilla-spidermonkey.github.io/sm-wasi-demo/data.json')).json(); + + const wasmModule = await loadWasm(data[0].url); + + const wasmFs = new WasmFs(); + + let wasi = new WASI({ + args: ['js.wasm', '-f', '/input.js'], + preopens: {'/': '/'}, + env: {}, + bindings: { + ...browserBindings, + fs: wasmFs.fs, + }, + }); + + let instance = await WebAssembly.instantiate(wasmModule, wasi.getImports(wasmModule)); + + wasmFs.fs.writeFileSync('/input.js', js); + + wasmFs.volume.fds[1].position = 0; + wasmFs.volume.fds[2].position = 0; + + wasmFs.fs.writeFileSync('/dev/stdin', ""); + wasmFs.fs.writeFileSync('/dev/stdout', ""); + wasmFs.fs.writeFileSync('/dev/stderr', ""); + + stdinCallback(wasmFs.fs); + + let lastStdout = ''; + wasmFs.fs.watch('/dev/stdout', () => { + const stdout = wasmFs.fs.readFileSync('/dev/stdout', 'utf8'); + + const newStdout = stdout.slice(lastStdout.length); + lastStdout = stdout; + + if (newStdout) { + const msgs = newStdout.split('\n'); + for (const x of msgs) { + if (x) ipcHandler(JSON.parse(x)); + } + } + }); + + try { + wasi.start(instance); + } catch (e) { + console.error(e); + } + + /* let stdout = wasmFs.fs.readFileSync('/dev/stdout').toString(); + let stderr = wasmFs.fs.readFileSync('/dev/stderr').toString(); + console.log({stdout, stderr}); */ +}; \ No newline at end of file diff --git a/engine/js/index.js b/engine/js/index.js new file mode 100644 index 0000000..476c7fc --- /dev/null +++ b/engine/js/index.js @@ -0,0 +1,34 @@ +import * as SpiderMonkey from './backends/spidermonkey.js'; +import * as Kiesel from './backends/kiesel.js'; + +import * as Runner from './ipc/outside.js'; + +const backends = { + kiesel: Kiesel, + spidermonkey: SpiderMonkey +}; +let backend = null; +export let backendName = null; + +export const setBackend = async (name, preload = true) => { + console.log('js backend is now', name); + if (name === null) { + backendName = null; + backend = null; + return; + } + + backendName = name.toLowerCase(); + + backend = backends[backendName]; + + if (preload) await run(null, ''); +}; + +export const run = async (doc, js) => { + if (!backend) return false; + + await Runner.run(backend, doc, js); + + return true; +}; \ No newline at end of file diff --git a/engine/js/ipc/inside.js b/engine/js/ipc/inside.js new file mode 100644 index 0000000..9bdb508 --- /dev/null +++ b/engine/js/ipc/inside.js @@ -0,0 +1,43 @@ +// JS WORLD +const ipc = { + send: msg => { + msg.id = Math.random(); + if (globalThis.Kiesel) { + Kiesel.print(msg, { pretty: true }); + } else { + print(JSON.stringify(msg)); + } + }, + + recv: () => { + if (globalThis.Kiesel) { + return eval(Kiesel.readLine()); + } else { + return JSON.parse(readline()); + } + } +}; + +class Element { + constructor(data) { + Object.assign(this, data); + } + + get textContent() { + ipc.send({ f: 'Element.getTextContent', ptr: this.ptr }); + return ipc.recv().value; + } + + set textContent(value) { + ipc.send({ f: 'Element.setTextContent', value, ptr: this.ptr }); + } +} + +globalThis.document = { + querySelector(selector) { + ipc.send({ f: 'document.querySelector', selector }); + const data = ipc.recv(); + + return new Element(data); + } +}; \ No newline at end of file diff --git a/engine/js/ipc/outside.js b/engine/js/ipc/outside.js new file mode 100644 index 0000000..6aef56b --- /dev/null +++ b/engine/js/ipc/outside.js @@ -0,0 +1,34 @@ +const insideJS = await (await fetch('engine/js/ipc/inside.js')).text(); + +const funcs = { + 'document.querySelector': ({ selector }, send, doc) => { + const el = doc.querySelector(selector); + send({ ptr: el.ptr }); + }, + + 'Element.getTextContent': ({ ptr }, send, doc) => { + const el = doc.getFromPtr(ptr); + send({ value: el.textContent }); + }, + + 'Element.setTextContent': ({ value, ptr }, send, doc) => { + const el = doc.getFromPtr(ptr); + el.textContent = value; + } +}; + +export const run = async (backend, doc, _js) => { + const js = insideJS + '\n\n' + _js.slice(); + let send; + + await backend.run(js, msg => { + // console.log('recv', msg); + funcs[msg.f](msg, send, doc); + }, + fs => { + send = msg => { + // console.log('send', msg); + fs.appendFileSync('/dev/stdin', JSON.stringify(msg) + '\n'); + }; + }) +}; \ No newline at end of file diff --git a/engine/layout.js b/engine/layout.js index 60395fc..159308d 100644 --- a/engine/layout.js +++ b/engine/layout.js @@ -47,12 +47,16 @@ window.colorScheme = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' // uh yeah this is just a constant :) const defaultFontSize = 16; // px +const byPtr = {}; export class LayoutNode extends Node { renderer = null; constructor(node, renderer) { super(); Object.assign(this, { ...node, renderer }); + this.ptr = parseInt(Math.random().toString().slice(2, 12)); + byPtr[this.ptr] = this; + const cache = k => { const f = this[k].bind(this); let cached; @@ -67,6 +71,10 @@ export class LayoutNode extends Node { if (this.tagName === 'img') this.image(); } + getFromPtr(ptr) { + return byPtr[ptr]; + } + matchesCSS(selector) { for (const x of selector) { // a, b, c (a OR b OR c) let match = true; @@ -193,6 +201,8 @@ export class LayoutNode extends Node { } display() { + if (this.tagName === 'noscript' && this.attrs.dynamic) return window._js.backendName ? 'none' : 'inline'; + // if (this.tagName === '#text') return 'inline'; return this.css().display; } diff --git a/engine/main.js b/engine/main.js index 9888f59..712e85a 100644 --- a/engine/main.js +++ b/engine/main.js @@ -2,7 +2,9 @@ import { Page } from './network.js'; import { HTMLParser } from './htmlparser.js'; import { constructLayout } from './layout.js'; import { Renderer } from './renderer.js'; +import * as JS from './js/index.js'; +window._js = JS; window.onpopstate = ({ state }) => { const url = state?.url ?? location.search.slice(1); @@ -103,19 +105,28 @@ load('data:text/html;base64,' + btoa(
  • c: dump parsed html
  • v: prompt to load url
  • h: go back to welcome page (here)
  • +
  • j: cycle JS engine (none -> SpiderMonkey -> Kiesel)
  • demo sites

    known issues

    -

    implemented

    - +

    javascript new!

    +

    ${shadow} has extremely experimental javascript support. this is not intended to be usable, rather a proof of concept. off by default.

    +

    bonus: you can choose which JS engine to use!

    + + + + 0

    bonus

    +

    implemented

    + + `), new URL('/', location.href)); }; diff --git a/engine/renderer.js b/engine/renderer.js index 34b5c70..dadc284 100644 --- a/engine/renderer.js +++ b/engine/renderer.js @@ -352,6 +352,10 @@ document.onmouseup = e => { else window.load(hoverEl.href.toString()); } + if (hoverEl) { + if (hoverEl.attrs.onclick) window._js.run(window._renderer.layout, hoverEl.attrs.onclick); + } + return false; }; @@ -375,6 +379,11 @@ document.onkeyup = e => { } if (k === 'v') window.load(prompt('url to load:')); if (k === 'h') window.welcome(); + if (k === 'j') { + const current = window._js.backendName; + const backends = [ null, 'spidermonkey', 'kiesel' ]; + window._js.setBackend(backends[(backends.indexOf(current) + 1) % 3]); + } }; document.onwheel = e => {