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 #[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 #[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 #[wasm_bindgen(js_name = "setRenderOptions")]
69 pub fn set_render_options(&mut self, value: RenderOptions) {
70 self.render = value.into();
71 }
72
73 #[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 #[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
143#[cfg(test)]
144mod tests {
145 #![allow(dead_code)]
146
147 use std::collections::HashMap;
148 use std::iter::FromIterator;
149
150 use wasm_bindgen_test::wasm_bindgen_test;
151
152 use crate::{Engine, ToHtmlResult};
153
154 #[wasm_bindgen_test]
155 fn it_should_render() {
156 let template = "<mjml><mj-body><mj-text>Hello World</mj-text></mj-body></mjml>";
157 let opts = Engine::new();
158 let result = opts.to_html(template);
159 assert!(matches!(result, ToHtmlResult::Success { .. }));
160 }
161
162 #[wasm_bindgen_test]
163 fn it_should_error() {
164 let template = "<mjml><mj-body><mj-text>Hello World";
165 let opts = Engine::new();
166 let result = opts.to_html(template);
167 assert!(matches!(result, ToHtmlResult::Error(_)));
168 }
169
170 #[wasm_bindgen_test]
171 fn it_should_render_with_include() {
172 let template = "<mjml><mj-body><mj-include path=\"/hello-world.mjml\" /></mj-body></mjml>";
173 let mut opts = Engine::new();
174 opts.set_parser_options(crate::ParserOptions {
175 include_loader: crate::parser::IncludeLoaderOptions::Memory(
176 crate::parser::MemoryIncludeLoaderOptions {
177 content: HashMap::from_iter([(
178 "/hello-world.mjml".to_string(),
179 "<mj-text>Hello World</mj-text>".to_string(),
180 )]),
181 },
182 ),
183 });
184 let result = opts.to_html(template);
185 assert!(matches!(result, ToHtmlResult::Success { .. }));
186 }
187}
188
189#[cfg(all(test, feature = "async"))]
190mod async_tests {
191 #![allow(dead_code)]
192
193 use std::collections::HashMap;
194 use std::iter::FromIterator;
195
196 use wasm_bindgen_test::wasm_bindgen_test;
197
198 use crate::{Engine, ToHtmlResult};
199
200 #[wasm_bindgen_test]
201 async fn it_should_render() {
202 let template = "<mjml><mj-body><mj-text>Hello World</mj-text></mj-body></mjml>";
203 let opts = Engine::new();
204 let result = opts.to_html_async(template).await;
205 assert!(matches!(result, ToHtmlResult::Success { .. }));
206 }
207
208 #[wasm_bindgen_test]
209 async fn it_should_error() {
210 let template = "<mjml><mj-body><mj-text>Hello World";
211 let opts = Engine::new();
212 let result = opts.to_html_async(template).await;
213 assert!(matches!(result, ToHtmlResult::Error(_)));
214 }
215
216 #[wasm_bindgen_test]
217 async fn it_should_render_with_include() {
218 let template = "<mjml><mj-body><mj-include path=\"/hello-world.mjml\" /></mj-body></mjml>";
219 let mut opts = Engine::new();
220 opts.set_async_parser_options(crate::AsyncParserOptions {
221 include_loader: crate::parser::AsyncIncludeLoaderOptions::Memory(
222 crate::parser::MemoryIncludeLoaderOptions {
223 content: HashMap::from_iter([(
224 "/hello-world.mjml".to_string(),
225 "<mj-text>Hello World</mj-text>".to_string(),
226 )]),
227 },
228 ),
229 });
230 let result = opts.to_html_async(template).await;
231 assert!(matches!(result, ToHtmlResult::Success { .. }));
232 }
233}