1use wasm_bindgen::prelude::*;
2use mech_core::*;
3use mech_syntax::*;
4use mech_interpreter::*;
5use wasm_bindgen::JsCast;
6use web_sys::{window, HtmlElement, HtmlInputElement, Node, Element};
7use std::rc::Rc;
8use std::cell::RefCell;
9
10use gloo_net::http::Request;
11use wasm_bindgen_futures::spawn_local;
12
13pub mod repl;
14
15pub use crate::repl::*;
16
17
18thread_local! {
22 pub static CURRENT_MECH: RefCell<Option<*mut WasmMech>> = RefCell::new(None);
23}
24
25#[macro_export]
26macro_rules! log {
27 ( $( $t:tt )* ) => {
28 web_sys::console::log_1(&format!( $( $t )* ).into());
29 }
30}
31
32#[wasm_bindgen(start)]
33pub fn main() -> Result<(), JsValue> {
34 Ok(())
38}
39
40
41fn run_mech_code(intrp: &mut Interpreter, code: &Vec<(String,MechSourceCode)>) -> MResult<Value> {
42 for (file, source) in code {
43 match source {
44 MechSourceCode::String(s) => {
45 let parse_result = parser::parse(&s.trim());
46 match parse_result {
47 Ok(tree) => {
48 let result = intrp.interpret(&tree);
49 return result;
50 },
51 Err(err) => return Err(err),
52 }
53 }
54 x => {
55 log!("Unsupported source code type: {:?}", x);
56 todo!();
57 },
58 }
59 }
60 Ok(Value::Empty)
61}
62
63#[wasm_bindgen]
64pub struct WasmMech {
65 interpreter: Interpreter,
66 repl_history: Vec<String>,
67 repl_history_index: Option<usize>,
68 repl_id: Option<String>,
69}
70
71#[wasm_bindgen]
72impl WasmMech {
73
74 #[wasm_bindgen(constructor)]
75 pub fn new() -> Self {
76 Self {
77 interpreter: Interpreter::new(0),
78 repl_history: Vec::new(),
79 repl_history_index: None,
80 repl_id: None,
81 }
82 }
83
84 #[wasm_bindgen]
85 pub fn out_string(&self) -> String {
86 self.interpreter.out.to_string()
87 }
88
89 #[wasm_bindgen]
90 pub fn clear(&mut self) {
91 self.interpreter = Interpreter::new(0);
92 }
93
94 #[wasm_bindgen]
95 pub fn attach_repl(&mut self, repl_id: &str) {
96 self.repl_id = Some(repl_id.to_string());
97 CURRENT_MECH.with(|c| *c.borrow_mut() = Some(self as *mut _));
100 let window = web_sys::window().expect("global window does not exists");
101 let document = window.document().expect("should have a document");
102 let container = document
103 .get_element_by_id(repl_id)
104 .expect("REPL element not found")
105 .dyn_into::<HtmlElement>()
106 .expect("Element should be HtmlElement");
107
108 let create_prompt: Rc<RefCell<Option<Box<dyn Fn()>>>> = Rc::new(RefCell::new(None));
109 let create_prompt_clone = create_prompt.clone();
110 let document_clone = document.clone();
111 let container_clone = container.clone();
112 let mech_output = container.clone();
113 let mech_output_for_event = mech_output.clone();
114
115 let closure = Closure::wrap(Box::new(move |_event: web_sys::MouseEvent| {
116 let window = web_sys::window().unwrap();
117 let selection = window.get_selection().unwrap().unwrap();
118
119 if selection.is_collapsed() {
121 if let Some(input) = mech_output
122 .owner_document()
123 .unwrap()
124 .get_element_by_id("repl-active-input")
125 {
126 let _ = input
127 .dyn_ref::<web_sys::HtmlElement>()
128 .unwrap()
129 .focus();
130 }
131 }
132 }) as Box<dyn FnMut(_)>);
133
134 mech_output_for_event.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref()).unwrap();
135 closure.forget();
136
137 *create_prompt.borrow_mut() = Some(Box::new(move || {
138 let line = document_clone.create_element("div").unwrap();
139 line.set_class_name("repl-line");
140
141 let prompt = document_clone.create_element("span").unwrap();
142 prompt.set_inner_html(">: ");
143 prompt.set_class_name("repl-prompt");
144
145 let input = document_clone.create_element("input")
146 .unwrap()
147 .dyn_into::<HtmlInputElement>()
148 .unwrap();
149 let input_for_closure = input.clone();
150 input.set_class_name("repl-input");
151 input.set_id("repl-active-input");
152 input.set_attribute("autocomplete", "off").unwrap();
153 input.unchecked_ref::<HtmlElement>().set_autofocus(true);
154
155 line.append_child(&prompt).unwrap();
156 line.append_child(&input).unwrap();
157 container_clone.append_child(&line).unwrap();
158 let _ = input.focus();
159
160 let document_inner = document_clone.clone();
161 let container_inner = container_clone.clone();
162 let create_prompt_inner = create_prompt_clone.clone();
163
164 let closure = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| {
166 match event.key().as_str() {
167 "Enter" => {
168 let code = input_for_closure.value();
169
170 let input_parent = input_for_closure.parent_node().expect("input should have a parent");
172
173 let input_span = document_inner.create_element("span").unwrap();
174 input_span.set_class_name("repl-code");
175 input_span.set_text_content(Some(&code));
176
177 input_parent.replace_child(&input_span, &input_for_closure).unwrap();
179
180 let _ = input_for_closure.focus();
181 input_for_closure.set_id("repl-active-input");
182
183 let result_line = document_inner.create_element("div").unwrap();
184 result_line.set_class_name("repl-result");
185 CURRENT_MECH.with(|mech_ref| {
187 if let Some(ptr) = *mech_ref.borrow() {
188 unsafe {
190 let mech = &mut *ptr;
191 let output = if !code.trim().is_empty() {
192 mech.repl_history.push(code.clone());
193 mech.repl_history_index = None;
194 mech.eval(&code)
195 } else {
196 "".to_string()
197 };
198 result_line.set_inner_html(&output);
199 container_inner.append_child(&result_line).unwrap();
200 mech.init();
201 }
202 }
203 });
204
205 if let Some(cb) = &*create_prompt_inner.borrow() {
206 cb();
207 }
208 }
209 "ArrowUp" => {
210 event.prevent_default();
211 CURRENT_MECH.with(|mech_ref| {
212 if let Some(ptr) = *mech_ref.borrow() {
213 unsafe {
214 let mech = &mut *ptr;
215 if !mech.repl_history.is_empty() {
216 let new_index = match mech.repl_history_index {
217 Some(i) if i > 0 => Some(i - 1),
218 None => Some(mech.repl_history.len().saturating_sub(1)),
219 Some(0) => Some(0),
220 _ => None,
221 };
222
223 if let Some(i) = new_index {
224 input_for_closure.set_value(&mech.repl_history[i]);
225 mech.repl_history_index = Some(i);
226 }
227 }
228 }
229 }
230 });
231 }
232 "ArrowDown" => {
233 event.prevent_default(); CURRENT_MECH.with(|mech_ref| {
235 if let Some(ptr) = *mech_ref.borrow() {
236 unsafe {
237 let mech = &mut *ptr;
238 if let Some(i) = mech.repl_history_index {
239 let new_index = if i + 1 < mech.repl_history.len() {
240 Some(i + 1)
241 } else {
242 None
243 };
244
245 if let Some(i) = new_index {
246 input_for_closure.set_value(&mech.repl_history[i]);
247 mech.repl_history_index = Some(i);
248 } else {
249 input_for_closure.set_value("");
250 mech.repl_history_index = None;
251 }
252 }
253 }
254 }
255 });
256 }
257 _ => (),
258 }
259 }) as Box<dyn FnMut(_)>);
260
261 input.add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref()).unwrap();
262 closure.forget();
263 }));
264
265 if let Some(cb) = &*create_prompt.borrow() {
266 cb();
267 };
268 }
269
270 pub fn eval(&mut self, input: &str) -> String {
271 if input.chars().nth(0) == Some(':') {
272 match parse_repl_command(&input.to_string()) {
273 Ok((_, repl_command)) => {
274 execute_repl_command(repl_command)
275 }
276 Err(x) => {
277 format!("Unrecognized command: {}", x)
278 }
279 }
280 } else {
281 let cmd = ReplCommand::Code(vec![("repl".to_string(),MechSourceCode::String(input.to_string()))]);
282 execute_repl_command(cmd)
283 }
284 }
285
286 #[wasm_bindgen]
287 pub fn add_clickable_event_listeners(&self) {
288 let window = web_sys::window().expect("global window does not exists");
289 let document = window.document().expect("expecting a document on window");
290 let clickable_elements = document.get_elements_by_class_name("mech-clickable");
292 for i in 0..clickable_elements.length() {
293 let element = clickable_elements.get_with_index(i).unwrap();
294 if element.get_attribute("data-click-bound").is_some() {
296 continue;
297 }
298 element.set_attribute("data-click-bound", "true").unwrap();
300 let id = element.id();
303 let parsed_id: Vec<&str> = id.split(":").collect();
304 let element_id = parsed_id[0].parse::<u64>().unwrap();
305 let interpreter_id = parsed_id[1].parse::<u64>().unwrap();
306 let symbols = match interpreter_id {
307 0 => self.interpreter.symbols(),
309 id => {
311 match self.interpreter.sub_interpreters.borrow().get(&id) {
312 Some(sub_interpreter) => sub_interpreter.symbols(),
313 None => {
314 log!("No sub interpreter found for id: {}", id);
315 continue;
316 }
317 }
318 }
319 };
320 let closure = Closure::wrap(Box::new(move || {
321 let window = web_sys::window().unwrap();
322 let document = window.document().unwrap();
323 let mech_output = document.get_element_by_id("mech-output").unwrap();
324 let last_child = mech_output.last_child().unwrap();
325 let symbols_brrw = symbols.borrow();
326
327 match symbols_brrw.get(element_id) {
328 Some(output) => {
329 let output_brrw = output.borrow();
330 let kind_str = html_escape(&format!("{}", output_brrw.kind()));
331 let result_html = format!(
332 "<div class=\"mech-output-kind\">{}</div><div class=\"mech-output-value\">{}</div>",
333 kind_str,
334 output_brrw.to_html()
335 );
336
337 let symbol_name = symbols_brrw.get_symbol_name_by_id(element_id).unwrap();
338
339 let prompt_line = document.create_element("div").unwrap();
340 prompt_line.set_class_name("repl-line");
341 let prompt_span = document.create_element("span").unwrap();
342 prompt_span.set_class_name("repl-prompt");
343 prompt_span.set_inner_html(">: ");
344 prompt_line.append_child(&prompt_span).unwrap();
345 let input_span = document.create_element("span").unwrap();
346 input_span.set_class_name("repl-code");
347 input_span.set_inner_html(&symbol_name);
348 prompt_line.append_child(&input_span).unwrap();
349 mech_output.insert_before(&prompt_line, Some(&last_child)).unwrap();
350
351 let result_line = document.create_element("div").unwrap();
352 result_line.set_class_name("repl-result");
353 result_line.set_inner_html(&result_html);
354 mech_output.insert_before(&result_line, Some(&last_child)).unwrap();
355
356 let output = CURRENT_MECH.with(|mech_ref| {
357 if let Some(ptr) = *mech_ref.borrow() {
358 unsafe {
359 (*ptr).repl_history.push(symbol_name.clone());
360 }
361 } else {
362 log!("[no interpreter]");
363 }
364 });
365
366 },
367 None => {
368 let error_message = format!("No value found for element id: {}", element_id);
369 let result_line = document.create_element("div").unwrap();
370 result_line.set_class_name("repl-result");
371 result_line.set_inner_html(&error_message);
372 mech_output.insert_before(&result_line, Some(&last_child)).unwrap();
373 }
374 }
375 mech_output.set_scroll_top(mech_output.scroll_height());
376 }) as Box<dyn Fn()>);
377
378
379 element.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref());
380 closure.forget();
381 }
382 }
383
384 #[wasm_bindgen]
385 pub fn init(&self) {
386 self.add_clickable_event_listeners();
387 }
388
389 #[wasm_bindgen]
390 pub fn render_values(&mut self) {
391 self.render_codeblock_output_values();
392 self.render_inline_values();
393 }
394
395 #[wasm_bindgen]
398 pub fn render_codeblock_output_values(&mut self) {
399 let window = web_sys::window().expect("global window does not exists");
400 let document = window.document().expect("expecting a document on window");
401 let programs = document.query_selector_all("[mech-interpreter-id]");
403 if let Ok(programs) = programs {
404 for i in 0..programs.length() {
405 let program_node = programs.item(i).expect("No node at index");
406 let program_el = program_node
407 .dyn_into::<Element>()
408 .expect("Node was not an Element");
409
410 let interpreter_id: String = program_el.get_attribute("mech-interpreter-id").unwrap();
412 let interpreter_id: u64 = interpreter_id.parse().unwrap();
413 let sub_interpreter_brrw = self.interpreter.sub_interpreters.borrow();
414 let intrp = match interpreter_id {
415 0 => &self.interpreter,
416 id => {
417 match sub_interpreter_brrw.get(&id) {
418 Some(sub_interpreter) => sub_interpreter,
419 None => {
420 log!("No sub interpreter found for id: {}", id);
421 continue;
422 }
423 }
424 }
425 };
426
427 let output_elements = program_el.query_selector_all(".mech-block-output");
429 if let Ok(output_elements) = output_elements {
430 for j in 0..output_elements.length() {
431 let block_node = output_elements.item(j).expect("No output element at index");
432 let block = block_node
433 .dyn_into::<web_sys::Element>()
434 .expect("Output node was not an Element");
435
436 let id = block.id();
440 let parsed_id: Vec<&str> = id.split(":").collect();
441 let output_id = parsed_id[0].parse::<u64>().unwrap();
442 let interpreter_id = parsed_id[1].parse::<u64>().unwrap();
443 let out_values = match interpreter_id {
445 0 => intrp.out_values.clone(),
447 id => {
449 match intrp.sub_interpreters.borrow().get(&id) {
450 Some(sub_interpreter) => sub_interpreter.out_values.clone(),
451 None => {
452 log!("No sub interpreter found for id: {}", id);
453 continue;
454 }
455 }
456 }
457 };
458
459 let out_value_brrw = out_values.borrow();
461 let output = match out_value_brrw.get(&output_id) {
462 Some(value) => value,
463 None => {
464 log!("No value found for output id: {}", output_id);
465 continue;
466 }
467 };
468 let kind_str = html_escape(&format!("{}",output.kind()));
470 let formatted_output = format!("<div class=\"mech-output-kind\">{}</div><div class=\"mech-output-value\">{}</div>", kind_str, output.to_html());
471 block.set_inner_html(&formatted_output);
472 }
473 }
474 }
475 }
476 }
477
478 #[wasm_bindgen]
479 pub fn render_inline_values(&mut self) {
480 let window = web_sys::window().expect("global window does not exists");
481 let document = window.document().expect("expecting a document on window");
482 let inline_elements = document.get_elements_by_class_name("mech-inline-mech-code");
483 let out_values_brrw = self.interpreter.out_values.borrow();
484 for j in 0..inline_elements.length() {
485 let inline_block = inline_elements.get_with_index(j).unwrap();
486 let inline_id = inline_block.id();
487 let inline_id: u64 = inline_id.parse().unwrap();
488
489 let inline_output = match out_values_brrw.get(&inline_id) {
490 Some(value) => value,
491 None => {
492 log!("No value found for inline output id: {}", inline_id);
493 continue;
494 }
495 };
496 let formatted_output = format!("{}", inline_output.to_string());
497 inline_block.set_inner_html(&formatted_output.trim());
498 }
499 }
500
501 #[wasm_bindgen]
502 pub fn run_program(&mut self, src: &str) {
503 match decode_and_decompress(&src) {
505 Ok(tree) => {
506 match self.interpreter.interpret(&tree) {
507 Ok(result) => {
508 log!("{}", result.pretty_print());
509 },
510 Err(err) => {
511 log!("{:?}", err);
512 }
513 }
514 },
515 Err(err) => {
516 match parse(src) {
517 Ok(tree) => {
518 match self.interpreter.interpret(&tree) {
519 Ok(result) => {
520 log!("{}", result.pretty_print());
521 },
522 Err(err) => {
523 log!("{:?}", err);
524 }
525 }
526 },
527 Err(parse_err) => {
528 log!("Error parsing program: {:?}", parse_err);
529 }
530 }
531 }
532 }
533 }
534}
535
536pub fn load_doc(doc: &str, element_id: String) {
537 let doc = doc.to_string();
538 spawn_local(async move {
539 let doc_mec = fetch_docs(&doc).await;
540 let doc_hash = hash_str(&doc_mec);
541 let window = web_sys::window().expect("global window does not exists");
542 let document = window.document().expect("expecting a document on window");
543 match parser::parse(&doc_mec) {
544 Ok(tree) => {
545 let mut formatter = Formatter::new();
546 formatter.html = true;
547 let doc_html = formatter.program(&tree);
548 let mut doc_intrp = Interpreter::new(doc_hash);
549 let doc_result = doc_intrp.interpret(&tree);
550 let output_element = document.get_element_by_id(&element_id).expect("REPL output element not found");
551 let children = output_element.children();
554 let len = children.length();
555 if len >= 2 {
556 let repl_result = children.item(len - 2).expect("Failed to get second-to-last child");
557 repl_result.set_attribute("mech-interpreter-id", &format!("{}",doc_hash)).unwrap();
558 repl_result.set_inner_html(&doc_html);
559 CURRENT_MECH.with(|mech_ref| {
560 if let Some(ptr) = *mech_ref.borrow() {
561 unsafe {
562 let mut mech = &mut *ptr;
563 mech.interpreter.sub_interpreters.borrow_mut().insert(doc_hash, Box::new(doc_intrp));
564 mech.render_codeblock_output_values();
565 }
566 }
567 })
568 } else {
569 web_sys::console::log_1(&"Not enough children in #mech-output to update.".into());
570 }
571 },
572 Err(err) => {
573 web_sys::console::log_1(&format!("Error formatting doc: {:?}", err).into());
574 }
575 }
576 });
577}
578
579async fn fetch_docs(doc: &str) -> String {
580 let parts: Vec<&str> = doc.split('/').collect();
582 if parts.len() >= 2 {
583 let machine = parts[0];
584 let doc = parts[1];
585 let url = format!("https://raw.githubusercontent.com/mech-machines/{}/main/docs/{}.mec", machine, doc);
586 match Request::get(&url).send().await {
587 Ok(response) => match response.text().await {
588 Ok(text) => {
589 text
590 }
591 Err(e) => {
592 web_sys::console::log_1(&format!("Error reading response text: {:?}", e).into());
593 "".to_string()
594 }
595 },
596 Err(err) => {
597 web_sys::console::log_1(&format!("Fetch error: {:?}", err).into());
598 "".to_string()
599 }
600 }
601 } else {
602 web_sys::console::log_1(&format!("Invalid doc format: {}", doc).into());
603 "".to_string()
604 }
605}