How to use function from other scope inside closure?

I'd like to convert text-area value into the Markdown, I successfully managed to display pure text from text-area into output container however I stuck on binding parser inside closure, so how could I use properly parser and convert new text and display in output container?

lib.rs

use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{Document};

use pulldown_cmark::{html, Options, Parser};

#[wasm_bindgen(start)]
pub fn run() -> Result<(), JsValue> {
    let window = web_sys::window().expect("should have a window in this context");
    let document = window.document().expect("window should have a document");

    setup_preview(&document);

    Ok(())
}

pub fn get_value(target: &web_sys::EventTarget) -> String {
    if let Some(input) = target.dyn_ref::<web_sys::HtmlTextAreaElement>() {
        return input.value();
    }
    "".into()
}

fn setup_preview(document: &Document) {
    let input = document.query_selector("textarea").unwrap().unwrap();
    let output = document.query_selector("output").unwrap().unwrap();

    let text: &str = "### Init";

    let parser = Parser::new_ext(text, Options::empty());
    let mut html_output_buf = String::new();
    html::push_html(&mut html_output_buf, parser);

    output.set_inner_html(&html_output_buf);

    let a = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| {
        let val = get_value(&event.target().unwrap());

        let mut new_html_output_buf: String = String::with_capacity(val.len() * 3 / 2);
        html::push_html(&mut new_html_output_buf, parser); // <-- bad line here :(

        output.set_inner_html(&new_html_output_buf);
    }) as Box<dyn FnMut(_)>);

    input.add_event_listener_with_callback("keydown", a.as_ref().unchecked_ref()).unwrap();

    a.forget();
}

Html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>Example</title>
  </head>
  <body>
    <main>
      <textarea autofocus="autofocus" id="editor"></textarea>
      <output></output>
    </main>
    <script src="index.js"></script>
  </body>
</html>

Cargo.toml

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
pulldown-cmark = "0.8.0"

[dependencies.web-sys]
version = "0.3"
features = ["Window", "Document", "KeyboardEvent", "HtmlTextAreaElement"]

Try doing:

fn setup_preview(document: &Document) {
    let input = document.query_selector("textarea").unwrap().unwrap();
    let output = document.query_selector("output").unwrap().unwrap();

    let text: &str = "### Init";

-   let parser = Parser::new_ext(text, Options::empty());
+   let mut parser = Parser::new_ext(text, Options::empty());
    let mut html_output_buf = String::new();
    html::push_html(&mut html_output_buf, parser);

    output.set_inner_html(&html_output_buf);

    let a = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| {
        let val = get_value(&event.target().unwrap());

        let mut new_html_output_buf: String = String::with_capacity(val.len() * 3 / 2);
-       html::push_html(&mut new_html_output_buf, parser); // <-- bad line here :(
+       html::push_html(&mut new_html_output_buf, parser.by_ref());

or move the Parser-creation logic inside the closure.

1 Like

this is great that I can borrow/reuse multiple times parser object by ref but this version doesn't throw any error and it doesn't return any string in output container :man_shrugging: The second approach to copy 1:1 the same code works like a harm but I'm not sure if in Rust DRY principle is commonly used, maybe it's fair enough? :thinking:

let mut parser = Parser::new_ext(text, Options::empty());
let mut html_output_buf = String::new();
html::push_html(&mut html_output_buf, parser.by_ref());

output.set_inner_html(&html_output_buf);

let a = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| {
    let val = get_value(&event.target().unwrap());

    // let mut new_html_output_buf: String = String::with_capacity(val.len() * 3 / 2);
    // html::push_html(&mut new_html_output_buf, parser.by_ref());

    let parser = Parser::new_ext(&val, Options::empty());
    let mut new_html_output_buf = String::new();
    html::push_html(&mut new_html_output_buf, parser);

    output.set_inner_html(&new_html_output_buf);
}) as Box<dyn FnMut(_)>);

input.add_event_listener_with_callback("keyup", a.as_ref().unchecked_ref()).unwrap();

a.forget();

EDIT: it seems that by_ref can utilize parser only once

in near future I'd like to add some events mapper in one place, eg.

let parser = Parser::new_ext(text, Options::empty())
        .map(|event| match event {
            Event::Text(text) => Event::Text(text.replace("Peter", "John").into()),
            _ => event,
        })

I learned that I can clone parser and use one string buffer, but I'm still missing how to pass new value into cloned parser or how to use the same events mapper, do you have any clue?

fn setup_preview(document: &Document) {
    let input = document.query_selector("textarea").unwrap().unwrap();
    let output = document.query_selector("output").unwrap().unwrap();

    let text: &str = "## This is Peter on ![holiday in Greece](pearl_beach.jpg).";

    let events: Vec<_> = Parser::new_ext(text, Options::empty())
        .map(|event| match event {
            Event::Text(text) => Event::Text(text.replace("Peter", "John").into()),
            _ => event,
        })
        .collect();

    let mut buf = String::with_capacity(text.len() * 3 / 2);

    html::push_html(&mut buf, events.clone().into_iter());

    output.set_inner_html(&buf);

    let a = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| {
        let val = get_value(&event.target().unwrap());

        buf.clear();
        // html::push_html(&mut buf, events.clone().into_iter()); // cannot reuse parser with new val
        html::push_html(&mut buf, Parser::new_ext(&val, Options::empty()));

        output.set_inner_html(&buf);
    }) as Box<dyn FnMut(_)>);

    input.add_event_listener_with_callback("keyup", a.as_ref().unchecked_ref()).unwrap();

    a.forget();
}

In your case I'd factor out the Parser part:

fn parser<'markdown_txt> (markdown_txt: &'markdown_txt str)
  -> impl 'markdown_txt + Iterator<Item = Event<'markdown_txt>>
{
    Parser::new_ext(markdown_txt, Option::empty())
        .map(|event| match event {
            | Event::Text(text) => Event::Text(text.replace("Peter", "John").into()),
            | _ => event,
        })
}

and then also factor a bit the buf.clear(); html… things:

let output = document.query_selector("output").unwrap().unwrap();
let mut buf = String::new();
let mut handle_md_text = move /* buf, output */ |md_text: &'_ str| {
    buf.clear();
    html::push_html(&mut buf, parser(md_text));
    output.set_inner_html(&buf);
}; // 'static + FnMut(&'_ str)

This way, the manual introduction followed by the conversion into a JS callback is simple:

handle_md_text(
    "## This is Peter on ![holiday in Greece](pearl_beach.jpg)."
);

let a = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| {
    handle_md_text(&get_value(&event.target().unwrap()));
}) as Box<dyn FnMut(_)>);
1 Like

It's so much cleaner now how actually closure works and how to write properly own wrapper function, thanks a lot!

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.