web repl
Some checks failed
webrepl / test (push) Failing after 36s

This commit is contained in:
trimill 2025-01-06 00:18:13 -05:00
parent 781e8ba3a3
commit af1dd02257
11 changed files with 480 additions and 3 deletions

View file

@ -0,0 +1,40 @@
name: webrepl
on: [push]
#on:
# push:
# branches:
# - main
jobs:
test:
runs-on: docker
container:
image: rust:1.83-alpine
steps:
- name: Install dependencies
run: apk add --no-cache git nodejs util-linux
- name: Install wasm-pack
run: cargo install wasm-pack
- name: Checkout
uses: actions/checkout@v4
- name: Build talc-web
run: wasm-pack build --release --no-typescript --no-pack --target web
working-directory: talc-web
- name: Save artifacts
uses: forgejo/upload-artifact@v4
with:
name: webrepl
path: |
*.html
*.js
*.css
pkg/
if-no-files-found: error
- name: Done
run: echo done!

92
Cargo.lock generated
View file

@ -29,6 +29,12 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "byteorder"
version = "1.5.0"
@ -144,8 +150,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
@ -163,6 +171,16 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "js-sys"
version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@ -288,6 +306,12 @@ dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
@ -427,9 +451,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "syn"
version = "2.0.94"
version = "2.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "987bc0be1cdea8b10216bd06e2ca407d40b9543468fafd3ddfb02f36e77f71f3"
checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a"
dependencies = [
"proc-macro2",
"quote",
@ -477,6 +501,16 @@ dependencies = [
"talc-macros",
]
[[package]]
name = "talc-web"
version = "0.1.0"
dependencies = [
"getrandom",
"talc-lang",
"talc-std",
"wasm-bindgen",
]
[[package]]
name = "unicode-ident"
version = "1.0.14"
@ -507,6 +541,60 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
dependencies = [
"cfg-if",
"once_cell",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
[[package]]
name = "windows-sys"
version = "0.52.0"

View file

@ -1,5 +1,5 @@
[workspace]
members = ["talc-lang", "talc-bin", "talc-std", "talc-macros"]
members = ["talc-lang", "talc-bin", "talc-std", "talc-macros", "talc-web"]
resolver = "2"

View file

@ -4,6 +4,7 @@
- [Installation](./install/installation.md)
- [Tour of the REPL](./install/repl.md)
- [The Web REPL](./install/webrepl.md)
# The language

View file

@ -0,0 +1,18 @@
# The Web REPL
As an alterative to the command-line REPL, you can try out Talc in the browser
at (https://talc.trimill.xyz/repl)[https://talc.trimill.xyz/repl]. Although
the full Talc language and standard library (aside from file I/O and process
control) are available, editing features are more limited than the command-line
interface - especially of note is the lack of syntax highlighting and tab
completion.
## Usage
Just like the command-line REPL, enter an expression and press Enter to
evaluate it. Use the up and down arrows to access input history.
For multiline editing, use Shift+Enter to insert a newline. Hold the Control
key while pressing the up or down arrows to move the cursor between lines.
To clear the screen, press Ctrl+L.

16
talc-web/Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "talc-web"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[lints]
workspace = true
[dependencies]
talc-lang = { path = "../talc-lang" }
talc-std = { path = "../talc-std", default-features = false, features = ["rand", "regex"] }
getrandom = { version = "0.2", features = ["js"] }
wasm-bindgen = "0.2"

16
talc-web/README.md Normal file
View file

@ -0,0 +1,16 @@
# Talc Web
The `talc-web` crate provides a WASM interface to execute Talc expressions
in a REPL. Together with the associated HTML, CSS, and JavaScript files, this
creates an alternative to the command-line REPL.
## Building
In the `talc-web` directory, run:
```bash
wasm-pack build --release --no-typescript --no-pack --target web
```
See (the main README.md)[../README.md] for more information about building
Talc.

29
talc-web/index.html Normal file
View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Talc Web REPL</title>
<script type="module" src="main.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="terminal">
<div id="history">
<div>Talc Web REPL</div>
<div>For help, see
<a href="https://talc.trimill.xyz/" target="_blank">the docs</a>
or
<a href="https://g.trimill.xyz/trimill/talc" target="_blank">the repository</a>
</div>
</div>
<div class="input_line">
<div id="prompt" class="prompt">&gt;&gt;&nbsp;</div>
<div id="input" style="display: inline-block" contenteditable="true"></div>
</div>
</div>
</body>
</html>

161
talc-web/main.js Normal file
View file

@ -0,0 +1,161 @@
"use strict";
import init, * as talc from "./pkg/talc_web.js";
await init();
/*
* DOM
*/
const $ = (q) => document.querySelector(q);
function newEl(tag, attrs, children) {
const el = document.createElement(tag);
for (const attr in attrs) {
el.setAttribute(attr, attrs[attr]);
}
for (const child of children) {
if (typeof(child) === "string") {
el.appendChild(document.createTextNode(child));
} else {
el.appendChild(child);
}
}
return el;
}
/*
* History
*/
let historyEntries = [""];
let historyIndex = 0;
let currentLine = "";
function prevHistory() {
const inputEl = $("#input");
if (historyIndex === 0) {
currentLine = inputEl.innerText;
}
historyIndex = Math.min(historyIndex+1, historyEntries.length);
const text = historyEntries[historyEntries.length - historyIndex];
inputEl.innerText = text;
document.getSelection().setPosition(inputEl, inputEl.childNodes.length);
}
function nextHistory() {
const inputEl = $("#input");
historyIndex = Math.max(historyIndex-1, 0);
let text;
if (historyIndex === 0) {
text = currentLine;
} else {
text = historyEntries[historyEntries.length - historyIndex];
}
inputEl.innerText = text;
document.getSelection().setPosition(inputEl, inputEl.childNodes.length);
}
/*
* REPL
*/
function nextLine() {
const newPrompt = newEl("div", {"class": "prompt"}, []);
newPrompt.innerHTML = $("#prompt").innerHTML;
const newInput = newEl("div", {}, []);
newInput.innerHTML = $("#input").innerHTML;
$("#history").appendChild(newEl(
"div",
{"class": "input_line"},
[newPrompt, newInput]
));
$("#input").innerHTML = "";
currentLine = "";
historyIndex = 0;
}
function execLine() {
const inputLine = $("#input").innerText;
nextLine();
historyEntries.push(inputLine);
try {
const fixedInput = inputLine.replaceAll("\u00a0", " ");
const out = talc.eval_line(fixedInput);
if (out !== undefined) {
$("#history").appendChild(newEl("div", {}, [out]));
}
} catch (e) {
$("#history").appendChild(newEl("div", {}, [
newEl("span", {"class": "error"}, ["Error: "]),
newEl("pre", {"class": "err_msg"}, [e.toString()])
]));
}
}
/*
* Input
*/
function handleCtrlKey(event) {
switch (event.code) {
case "KeyL":
event.preventDefault();
$("#history").innerHTML = "";
break;
case "KeyC":
event.preventDefault();
nextLine();
break;
}
}
function keyPressed(event) {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
execLine()
} else if (event.ctrlKey) {
handleCtrlKey(event);
} else if (event.key === "ArrowUp") {
event.preventDefault();
prevHistory();
} else if (event.key === "ArrowDown") {
event.preventDefault();
nextHistory();
}
window.scrollTo(0, document.body.scrollHeight);
}
/*
* Listeners
*/
$("#input").addEventListener("keydown", keyPressed);
$("#input").focus();
$("#terminal").addEventListener("click", (event) => {
if (event.target.id === "terminal" || event.target.id === "input") {
$("#input").focus();
}
})
/*
* Query parameter
*/
const urlParams = new URLSearchParams(window.location.search);
const initialLine = urlParams.get("eval");
if (initialLine !== null) {
$("#input").innerText = initialLine;
execLine();
}

51
talc-web/src/lib.rs Normal file
View file

@ -0,0 +1,51 @@
use std::{cell::RefCell, rc::Rc};
use talc_lang::{
compiler, parser,
symbol::{symbol, Symbol},
value::Value,
vm::Vm,
};
use wasm_bindgen::prelude::wasm_bindgen;
type BoxError = Box<dyn std::error::Error>;
struct Talc {
vm: Vm,
globals: Vec<Symbol>,
}
impl Talc {
pub fn eval_line(&mut self, line: &str) -> Result<Option<String>, BoxError> {
let expr = parser::parse(line)?;
let func = compiler::compile_repl(&expr, &mut self.globals)?;
let func = Rc::new(func);
let res = self.vm.run_function(func.clone(), vec![func.into()])?;
let res_str = (res != Value::Nil).then(|| format!("{res:#}"));
self.vm.set_global(symbol!("_"), res);
Ok(res_str)
}
}
thread_local! {
static TALC: RefCell<Talc> = {
let mut vm = Vm::new(128, Vec::new());
talc_std::load_all(&mut vm);
let globals = vec![symbol!("_")];
vm.set_global(symbol!("_"), Value::Nil);
RefCell::new(Talc { vm, globals })
}
}
#[wasm_bindgen(start)]
pub fn start() {
TALC.with(|_| ());
}
#[wasm_bindgen]
pub fn eval_line(line: &str) -> Result<Option<String>, String> {
TALC.with_borrow_mut(|t| t.eval_line(line))
.map_err(|e| e.to_string())
}

57
talc-web/style.css Normal file
View file

@ -0,0 +1,57 @@
body {
font-family: monospace;
font-size: 14pt;
background-color: #14171d;
color: #c7c6c3;
margin: 20px;
}
.input_line {
display: flex;
}
#prompt {
margin-bottom: 100px;
}
#input {
width: 100%;
}
#input:focus {
outline: none;
}
div, pre {
margin: 0;
padding: 0;
}
#terminal {
min-height: calc(100vh - 60px - 8px);
padding: 10px;
border: 4px solid #4d4754;
border-radius: 6px;
}
a {
color: #82bfb3;
}
a:visited {
color: #82bfb3;
}
pre.err_msg {
display: inline;
}
.error {
color: #cc5c5c;
}
.prompt {
color: #789ebf;
}