mrml_wasm/
lib.rs

1#![allow(clippy::empty_docs)]
2
3mod parser;
4mod render;
5
6use std::rc::Rc;
7
8use wasm_bindgen::prelude::*;
9
10pub use crate::parser::*;
11pub use crate::render::*;
12
13#[inline]
14fn to_html(
15    input: &str,
16    parser_options: &mrml::prelude::parser::ParserOptions,
17    render_options: &mrml::prelude::render::RenderOptions,
18) -> Result<(String, Vec<Warning>), ToHtmlError> {
19    let element = mrml::parse_with_options(input, parser_options)?;
20    let html = element.element.render(render_options)?;
21    Ok((html, Warning::from_vec(element.warnings)))
22}
23
24#[cfg(feature = "async")]
25#[inline]
26async fn to_html_async(
27    input: &str,
28    parser_options: std::sync::Arc<mrml::prelude::parser::AsyncParserOptions>,
29    render_options: &mrml::prelude::render::RenderOptions,
30) -> Result<(String, Vec<Warning>), ToHtmlError> {
31    let element = mrml::async_parse_with_options(input, parser_options).await?;
32    let html = element.element.render(render_options)?;
33    Ok((html, Warning::from_vec(element.warnings)))
34}
35
36#[derive(Debug, Default)]
37#[wasm_bindgen]
38pub struct Engine {
39    parser: Rc<mrml::prelude::parser::ParserOptions>,
40    #[cfg(feature = "async")]
41    async_parser: std::sync::Arc<mrml::prelude::parser::AsyncParserOptions>,
42    render: mrml::prelude::render::RenderOptions,
43}
44
45#[wasm_bindgen]
46impl Engine {
47    #[wasm_bindgen(constructor)]
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    /// Defines the parsing options.
53    #[allow(clippy::arc_with_non_send_sync)]
54    #[wasm_bindgen(js_name = "setParserOptions")]
55    pub fn set_parser_options(&mut self, value: ParserOptions) {
56        self.parser = Rc::new(value.into());
57    }
58
59    /// Defines the async parsing options.
60    #[cfg(feature = "async")]
61    #[allow(clippy::arc_with_non_send_sync)]
62    #[wasm_bindgen(js_name = "setAsyncParserOptions")]
63    pub fn set_async_parser_options(&mut self, value: AsyncParserOptions) {
64        self.async_parser = std::sync::Arc::new(value.into());
65    }
66
67    /// Defines the rendering options.
68    #[wasm_bindgen(js_name = "setRenderOptions")]
69    pub fn set_render_options(&mut self, value: RenderOptions) {
70        self.render = value.into();
71    }
72
73    /// Renders the mjml input into html.
74    #[wasm_bindgen(js_name = "toHtml")]
75    pub fn to_html(&self, input: &str) -> ToHtmlResult {
76        match to_html(input, &self.parser, &self.render) {
77            Ok((content, warnings)) => ToHtmlResult::Success { content, warnings },
78            Err(error) => ToHtmlResult::Error(error),
79        }
80    }
81
82    /// Renders the mjml input into html.
83    #[cfg(feature = "async")]
84    #[wasm_bindgen(js_name = "toHtmlAsync")]
85    pub async fn to_html_async(&self, input: &str) -> ToHtmlResult {
86        match to_html_async(input, self.async_parser.clone(), &self.render).await {
87            Ok((content, warnings)) => ToHtmlResult::Success { content, warnings },
88            Err(error) => ToHtmlResult::Error(error),
89        }
90    }
91}
92
93#[derive(Debug, serde::Deserialize, serde::Serialize, tsify::Tsify)]
94#[serde(rename_all = "camelCase", tag = "origin")]
95#[tsify(into_wasm_abi)]
96pub enum ToHtmlError {
97    Parser {
98        message: String,
99        details: ParserError,
100    },
101    Render {
102        message: String,
103    },
104}
105
106impl From<mrml::prelude::parser::Error> for ToHtmlError {
107    fn from(value: mrml::prelude::parser::Error) -> Self {
108        ToHtmlError::Parser {
109            message: value.to_string(),
110            details: value.into(),
111        }
112    }
113}
114
115impl From<mrml::prelude::render::Error> for ToHtmlError {
116    fn from(value: mrml::prelude::render::Error) -> Self {
117        ToHtmlError::Render {
118            message: value.to_string(),
119        }
120    }
121}
122
123#[derive(Debug, serde::Serialize, tsify::Tsify)]
124#[serde(rename_all = "camelCase", tag = "type")]
125#[tsify(into_wasm_abi)]
126pub enum ToHtmlResult {
127    Success {
128        content: String,
129        warnings: Vec<Warning>,
130    },
131    Error(ToHtmlError),
132}
133
134impl ToHtmlResult {
135    pub fn into_success(self) -> String {
136        match self {
137            Self::Success { content, .. } => content,
138            Self::Error(inner) => panic!("unexpected error {:?}", inner),
139        }
140    }
141}
142
143impl From<ToHtmlResult> for JsValue {
144    fn from(value: ToHtmlResult) -> Self {
145        serde_wasm_bindgen::to_value(&value).unwrap()
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    #![allow(dead_code)]
152
153    use std::collections::HashMap;
154    use std::iter::FromIterator;
155
156    use wasm_bindgen_test::wasm_bindgen_test;
157
158    use crate::{Engine, ToHtmlResult};
159
160    #[wasm_bindgen_test]
161    fn it_should_render() {
162        let template = "<mjml><mj-body><mj-text>Hello World</mj-text></mj-body></mjml>";
163        let opts = Engine::new();
164        let result = opts.to_html(template);
165        assert!(matches!(result, ToHtmlResult::Success { .. }));
166    }
167
168    #[wasm_bindgen_test]
169    fn it_should_error() {
170        let template = "<mjml><mj-body><mj-text>Hello World";
171        let opts = Engine::new();
172        let result = opts.to_html(template);
173        assert!(matches!(result, ToHtmlResult::Error(_)));
174    }
175
176    #[wasm_bindgen_test]
177    fn it_should_render_with_include() {
178        let template = "<mjml><mj-body><mj-include path=\"/hello-world.mjml\" /></mj-body></mjml>";
179        let mut opts = Engine::new();
180        opts.set_parser_options(crate::ParserOptions {
181            include_loader: crate::parser::IncludeLoaderOptions::Memory(
182                crate::parser::MemoryIncludeLoaderOptions {
183                    content: HashMap::from_iter([(
184                        "/hello-world.mjml".to_string(),
185                        "<mj-text>Hello World</mj-text>".to_string(),
186                    )]),
187                },
188            ),
189        });
190        let result = opts.to_html(template);
191        assert!(matches!(result, ToHtmlResult::Success { .. }));
192    }
193}
194
195#[cfg(all(test, feature = "async"))]
196mod async_tests {
197    #![allow(dead_code)]
198
199    use std::collections::HashMap;
200    use std::iter::FromIterator;
201
202    use wasm_bindgen_test::wasm_bindgen_test;
203
204    use crate::{Engine, ToHtmlResult};
205
206    #[wasm_bindgen_test]
207    async fn it_should_render() {
208        let template = "<mjml><mj-body><mj-text>Hello World</mj-text></mj-body></mjml>";
209        let opts = Engine::new();
210        let result = opts.to_html_async(template).await;
211        assert!(matches!(result, ToHtmlResult::Success { .. }));
212    }
213
214    #[wasm_bindgen_test]
215    async fn it_should_error() {
216        let template = "<mjml><mj-body><mj-text>Hello World";
217        let opts = Engine::new();
218        let result = opts.to_html_async(template).await;
219        assert!(matches!(result, ToHtmlResult::Error(_)));
220    }
221
222    #[wasm_bindgen_test]
223    async fn it_should_render_with_include() {
224        let template = "<mjml><mj-body><mj-include path=\"/hello-world.mjml\" /></mj-body></mjml>";
225        let mut opts = Engine::new();
226        opts.set_async_parser_options(crate::AsyncParserOptions {
227            include_loader: crate::parser::AsyncIncludeLoaderOptions::Memory(
228                crate::parser::MemoryIncludeLoaderOptions {
229                    content: HashMap::from_iter([(
230                        "/hello-world.mjml".to_string(),
231                        "<mj-text>Hello World</mj-text>".to_string(),
232                    )]),
233                },
234            ),
235        });
236        let result = opts.to_html_async(template).await;
237        assert!(matches!(result, ToHtmlResult::Success { .. }));
238    }
239}