Skip to content

Commit

Permalink
js: initial add!!
Browse files Browse the repository at this point in the history
  • Loading branch information
CanadaHonk committed Oct 25, 2023
1 parent e1c0dfb commit b1b8324
Show file tree
Hide file tree
Showing 8 changed files with 304 additions and 3 deletions.
74 changes: 74 additions & 0 deletions engine/js/backends/kiesel.js
Original file line number Diff line number Diff line change
@@ -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/[email protected]');
0, browserBindings = (await import('https://esm.sh/@wasmer/[email protected]/lib/bindings/browser')).default;

// const { WasmFs } = await import('https://esm.sh/@wasmer/[email protected]');
0, { WasmFs } = await import('https://esm.sh/@wasmer/[email protected]');
// 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}); */
};
72 changes: 72 additions & 0 deletions engine/js/backends/spidermonkey.js
Original file line number Diff line number Diff line change
@@ -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/[email protected]');
0, browserBindings = (await import('https://esm.sh/@wasmer/[email protected]/lib/bindings/browser')).default;

// const { WasmFs } = await import('https://esm.sh/@wasmer/[email protected]');
0, { WasmFs } = await import('https://esm.sh/@wasmer/[email protected]');
// 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}); */
};
34 changes: 34 additions & 0 deletions engine/js/index.js
Original file line number Diff line number Diff line change
@@ -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;
};
43 changes: 43 additions & 0 deletions engine/js/ipc/inside.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
34 changes: 34 additions & 0 deletions engine/js/ipc/outside.js
Original file line number Diff line number Diff line change
@@ -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');
};
})
};
10 changes: 10 additions & 0 deletions engine/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
31 changes: 28 additions & 3 deletions engine/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -103,19 +105,28 @@ load('data:text/html;base64,' + btoa(
<li><b>c</b>: dump parsed html</li>
<li><b>v</b>: prompt to load url</li>
<li><b>h</b>: go back to welcome page (here)</li>
<li><b>j</b>: cycle JS engine (none -> SpiderMonkey -> Kiesel)</li>
</ul>
<h2>demo sites</h2>
<ul>${demos.map(x => `<li><a href="${x[0]}">${x[0]}</a> (${x[1]})</li>`).join('\n')}</ul>
<h2>known issues</h2>
<ul>
<li>basically every modern site doesn't work ;)
<li>basically every site doesn't work ;)
<li>performance is bad. this is because ${shadow} currently does ~0 optimizations.<br> <b>we recompute the entire layout every frame</b>, no (in)validation. it can be much better later :)</li>
<li>no text wrapping yet (!)
</ul>
<h2>implemented</h2>
<ul>${supported.map(x => `<li>${x.replaceAll('<', '&lt;').replaceAll('>', '&gt;')}</li>`).join('\n')}</ul>
<h2>javascript <span class="new">new!</span></h2>
<p>${shadow} has extremely experimental javascript support. this is not intended to be usable, rather a proof of concept. off by default.</p>
<p>bonus: <b>you can choose which JS engine to use!</b></p>
<ul>
<li><a href="https://spidermonkey.dev">SpiderMonkey</a> (Firefox's JS engine)</li>
<li><a href="https://kiesel.dev">Kiesel</a> (a WIP engine from scratch in Zig)</li>
</ul>
<button onclick="let el = document.querySelector('#counter'); el.textContent = parseInt(el.textContent) + 1">click me!</button>&nbsp;<span id="counter">0</span><noscript dynamic=true>(you have JS disabled, press J)</noscript>
<h2>bonus</h2>
<ul>
Expand All @@ -125,6 +136,9 @@ load('data:text/html;base64,' + btoa(
<li><a href="https://github.com/CanadaHonk/shadow" target="_parent">source code</a> (external)</li>
</ul>
<h2>implemented</h2>
<ul>${supported.map(x => `<li>${x.replaceAll('<', '&lt;').replaceAll('>', '&gt;')}</li>`).join('\n')}</ul>
<style>
body {
font-family: sans-serif;
Expand All @@ -143,6 +157,17 @@ li {
h2 {
margin-top: 1.5em;
}
.new {
color: red;
font-size: 0.8em;
}
noscript {
margin-left: 20px;
color: gray;
font-size: 0.8em;
}
</style>
</body>`), new URL('/', location.href));
};
Expand Down
9 changes: 9 additions & 0 deletions engine/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand All @@ -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 => {
Expand Down

0 comments on commit b1b8324

Please sign in to comment.