mech_wasm/
lib.rs

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
18// This monstrosity lets us pass a references to WasmMech to callbacks and such.
19// Using it is unsafe. But we trust that the WasmMech instance will be around
20// for the lifetime of the website.
21thread_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  //let mut wasm_mech = WasmMech::new();
35  //wasm_mech.init();
36  //wasm_mech.run_program("1 + 1");
37  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    // Assign self to the CURRENT_MECH thread local variable
98    // so that we can access it from the callbacks. Unsafe.
99    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      // Only focus the input if the selection is collapsed (no text is selected)
120      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("&gt;: ");
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      // Handler for keyboard events
165      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            // Replace input field with text
171            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            // Replace the input element in the DOM
178            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            // SAFELY call back into WasmMech
186            CURRENT_MECH.with(|mech_ref| {
187              if let Some(ptr) = *mech_ref.borrow() {
188                // UNSAFE but valid: we trust that `self` lives
189                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(); // prevent cursor jump
234            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    // Set up a click event listener for all elements with the class "mech-clickable"
291    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      // Skip if listener already added
295      if element.get_attribute("data-click-bound").is_some() {
296        continue;
297      }
298      // Mark it as handled
299      element.set_attribute("data-click-bound", "true").unwrap();
300      // the element id is formed like this : let id = format!("{}:{}",hash_str(&name),self.interpreter_id);
301      // so we need to parse it to get the id and the interpreter id
302      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        // if the interpreter id is 0, we are in the main interpreter
308        0 => self.interpreter.symbols(), 
309        // if the interpreter id is not 0, we are in a sub interpreter
310        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("&gt;: ");
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  // Write block output each element that needs it, rendering it appropriately
396  // based on its data type.
397  #[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    // Get all elements with an attribute of "mech-interpreter-id"
402    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        // Get the mech-interpreter-id attribute from the element
411        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        // Get all elements with the class "mech-block-output" that are children of the program element
428        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            // the id looks like this
437            // output_id:interpreter_id
438            // so we need to parse it to get the id and the interpreter id
439            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            // get the interpreter id from the block id
444            let out_values = match interpreter_id {
445              // if the interpreter id is 0, we are in the main interpreter
446              0 => intrp.out_values.clone(), 
447              // if the interpreter id is not 0, we are in a sub interpreter
448              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            // get the output id from the block id
460            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            // set the inner html of the block to the output value html
469            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    // Decompress the string into a Program
504    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        // Get the second to last element of mech-output. It should be a repl-result from when teh user pressed enter.
552        // Set the inner html of the repl result element to be the formatted doc.
553        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  // the doc will be formatted as machine/doc
581  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}