sunng87 / handlebars-rust

Rust templating with Handlebars
MIT License
1.28k stars 138 forks source link

WebAssembly API for JavaScript #208

Open sunng87 opened 6 years ago

sunng87 commented 6 years ago

Finally we have (wasm-bindgen)[https://github.com/alexcrichton/wasm-bindgen] to expose Rust struct to JavaScript, so we should be able to create Handlebars instance from JavaScript, and call any API from it.

This has made it possible to create a JavaScript interface for handlebars-rust. The next step will be allowing JavaScript implemented helpers and directives being registered in handlebars-rust.

fabienjuif commented 5 years ago

First things I saw that block this process is this issue from same-file, a dependency used by walkdir: https://github.com/BurntSushi/same-file/issues/42

sunng87 commented 5 years ago

Yes, this is known and I have a feature no_dir_source that disable walkdir

fabienjuif commented 5 years ago

Ha nice ๐Ÿ‘ Sorry then

sunng87 commented 5 years ago

Never mind. By the way, if you are interested in a WebAssembly/JavaScript API for handlebars, feel free to discuss here. Things I have been thinking about:

kitwon commented 2 years ago

In recent days I have tried to wrap register_helper using WASM. I have found two apis Function::new_no_args and Function::from in js-sys package. It is mostly like javascript eval function, so we can save javascript function template and call it like eval when calling rust helper. ๐Ÿค”

Here is the code:

use handlebars::{
  Context, Handlebars as Registry, Helper, HelperDef, HelperResult, Output, RenderContext,
  RenderError,
};
use js_sys::Function;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct HelperOptions {
  template_ctx: JsValue,
  inverse_ctx: JsValue,
}

#[allow(clippy::new_without_default)]
#[wasm_bindgen]
impl HelperOptions {
  pub fn new() -> Self {
    HelperOptions {
      template_ctx: JsValue::undefined(),
      inverse_ctx: JsValue::undefined(),
    }
  }

  pub fn template(&mut self, ctx: JsValue) -> String {
    self.template_ctx = ctx;
    "$$TEMPLATE$$".to_string()
  }

  pub fn inverse(&mut self, ctx: JsValue) -> String {
    self.inverse_ctx = ctx;
    "$$INVERSE$$".to_string()
  }
}

pub struct JsHelper {
  pub js_fn_tpl: String,
}

impl HelperDef for JsHelper {
  fn call<'reg: 'rc, 'rc>(
    &self,
    h: &Helper<'reg, 'rc>,
    _: &'reg Registry<'reg>,
    _: &'rc Context,
    _: &mut RenderContext<'reg, 'rc>,
    out: &mut dyn Output,
  ) -> HelperResult {
    // Change function template to actual js function
    let js_fn = Function::new_no_args(&self.js_fn_tpl);
    let js_helper = Function::from(js_fn.call0(&JsValue::null()).unwrap());

    let value = h
      .param(0)
      .ok_or_else(|| RenderError::new("Param not found in js helper"))
      .unwrap();
    let data = JsValue::from_serde(value.value()).unwrap();

    let options = HelperOptions::new();
    let result = js_helper
      .call2(&JsValue::null(), &data, &options.into())
      .unwrap();
    let result = result.as_string().unwrap();
    out.write(result.as_str())?;
    Ok(())
  }
}

And we can hack the js callback param options like the struct HelperOptions. When calling the fn and inverse function, tag it in return string and save the call info. Parse the result string with the call info in rust after js_helper call finish.

๐Ÿ˜‚ Not sure if it is the "best practices" or just a โ€œtrickโ€. Do y'all have a suggested workaround for this? (repo here)

sunng87 commented 2 years ago

Perhaps we can use HelperOptions to hold enough information(render_context, registry and etc) and make template() and inverse() call that actually renders the template with that information.

kitwon commented 2 years ago

๐Ÿค” Ah,ย I have tried but it does not work, because the wasm_bindgen cannot have lifetime parameters.

Maybe we can use a static to hold that information, but for me it is another complicated workaround. Is it possible to ask you to provide more details about it?

sunng87 commented 2 years ago

I see. The data exchange between js and webassembly still requires some hack. I would prefer some result string/code for template() and inverse() over static holder, since it's more predictable and I tried best to avoid global state in handlebars.

kitwon commented 2 years ago

Yep, if you have plans to add this feature, let me know if you need a hand.

sunng87 commented 2 years ago

Thank you! This feature is welcomed. Do you have a full example of your current usage of this library via wasm and javascript? I'm just wondering the scenario of defining helpers via javascript.

kitwon commented 2 years ago

Not yet. I was using handlebars.js and doing some surveys about other more efficient template engines.

But I think the first step is to do some performance test for the js_helper, because the data exchange cost here is a black box. But I need to start doing this after 4 days and will comment here again if any questions arise.

djahandarie commented 1 year ago

We are also considering using this library as a (wasm) replacement for handlebars.js, as handlebars.js requires unsafe-eval and therefore will not be possible to use in MV3 extensions at least on firefox. But we need to have as-close-as-possible support for existing users and all their handlebars scripts, so cannot easily drop in another library. This would require being able to use our existing JS helpers, or reimplementing all of those in Rust, which might be difficult/impossible.