Skip to main content

rust_wasm_dodrio_templating/
html_template_mod.rs

1//! **html_template_mod**  
2//! Html templating for dodrio, generic code for a standalone library.
3//! The implementation is in another file where RootRenderingComponents
4//! implement the trait HtmlTemplating
5
6// region: use
7use reader_for_microxml::*;
8
9use dodrio::{
10    builder::{text, ElementBuilder},
11    bumpalo::{self},
12    Attribute, Listener, Node, RenderContext, RootRender, VdomWeak,
13};
14use unwrap::unwrap;
15// endregion: use
16
17/// Svg elements are different because they have a namespace
18#[derive(Clone, Copy)]
19pub enum HtmlOrSvg {
20    /// html element
21    Html,
22    /// svg element
23    Svg,
24}
25
26/// the RootRenderingComponent struct must implement this trait
27/// it must have the fields for local_route and html_template fields
28pub trait HtmlTemplating {
29    // region: methods to be implemented for a specific project
30    // while rendering, cannot mut rrc
31    fn replace_with_string(&self, fn_name: &str) -> String;
32    fn retain_next_node_or_attribute<'a>(&self, fn_name: &str) -> bool;
33    fn replace_with_nodes<'a>(&self, cx: &mut RenderContext<'a>, fn_name: &str) -> Vec<Node<'a>>;
34    fn set_event_listener(
35        &self,
36        fn_name: String,
37    ) -> Box<dyn Fn(&mut dyn RootRender, VdomWeak, web_sys::Event) + 'static>;
38    // endregion: methods to be implemented
39
40    // region: generic code (in trait definition)
41
42    /// get root element Node.   
43    /// I wanted to use dodrio::Node, but it has only private methods.  
44    /// I must use dodrio element_builder.  
45    fn render_template<'a>(
46        &self,
47        cx: &mut RenderContext<'a>,
48        html_template: &str,
49        html_or_svg_parent: HtmlOrSvg,
50    ) -> Result<Node<'a>, String> {
51        let mut reader_for_microxml = ReaderForMicroXml::new(html_template);
52        let mut dom_path = Vec::new();
53        let mut root_element;
54        let mut html_or_svg_local = html_or_svg_parent;
55        let bump = cx.bump;
56        // the first element must be root and is special
57        #[allow(clippy::single_match_else, clippy::wildcard_enum_match_arm)]
58        match reader_for_microxml.next() {
59            None => {
60                // return error
61                return Err("Error: no root element".to_owned());
62            }
63            Some(result_token) => {
64                match result_token {
65                    Result::Err(e) => {
66                        // return error
67                        return Err(format!("Error: {}", e));
68                    }
69                    Result::Ok(token) => {
70                        match token {
71                            Token::StartElement(name) => {
72                                dom_path.push(name.to_owned());
73                                let name = bumpalo::format!(in bump, "{}",name).into_bump_str();
74                                root_element = ElementBuilder::new(bump, name);
75                                if name == "svg" {
76                                    html_or_svg_local = HtmlOrSvg::Svg;
77                                }
78                                if let HtmlOrSvg::Svg = html_or_svg_local {
79                                    // svg elements have this namespace
80                                    root_element =
81                                        root_element.namespace(Some("http://www.w3.org/2000/svg"));
82                                }
83                                // recursive function can return error
84                                match self.fill_element_builder(
85                                    &mut reader_for_microxml,
86                                    root_element,
87                                    cx,
88                                    html_or_svg_local,
89                                    &mut dom_path,
90                                ) {
91                                    // the methods are move, so I have to return the moved value
92                                    Ok(new_root_element) => root_element = new_root_element,
93                                    Err(err) => {
94                                        return Err(err);
95                                    }
96                                }
97                            }
98                            _ => {
99                                // return error
100                                return Err("Error: no root element".to_owned());
101                            }
102                        }
103                    }
104                }
105            }
106        }
107        // return
108        Ok(root_element.finish())
109    }
110    /// Recursive function to fill the Element with attributes and sub-nodes(Element, Text, Comment).  
111    /// Moves & Returns ElementBuilder or error.  
112    /// I must `move` ElementBuilder because its methods are all `move`.  
113    /// It makes the code less readable. It is only good for chaining and type changing.  
114    #[allow(clippy::too_many_lines, clippy::type_complexity)]
115    fn fill_element_builder<'a>(
116        &self,
117        reader_for_microxml: &mut ReaderForMicroXml,
118        mut element: ElementBuilder<
119            'a,
120            bumpalo::collections::Vec<'a, Listener<'a>>,
121            bumpalo::collections::Vec<'a, Attribute<'a>>,
122            bumpalo::collections::Vec<'a, Node<'a>>,
123        >,
124        cx: &mut RenderContext<'a>,
125        html_or_svg_parent: HtmlOrSvg,
126        dom_path: &mut Vec<String>,
127    ) -> Result<
128        ElementBuilder<
129            'a,
130            bumpalo::collections::Vec<'a, Listener<'a>>,
131            bumpalo::collections::Vec<'a, Attribute<'a>>,
132            bumpalo::collections::Vec<'a, Node<'a>>,
133        >,
134        String,
135    > {
136        let mut replace_string: Option<String> = None;
137        let mut replace_vec_nodes: Option<Vec<Node>> = None;
138        let mut replace_boolean: Option<bool> = None;
139        let mut html_or_svg_local;
140        let bump = cx.bump;
141        // loop through all the siblings in this iteration
142        loop {
143            // the children inherits html_or_svg from the parent, but cannot change the parent
144            html_or_svg_local = html_or_svg_parent;
145            match reader_for_microxml.next() {
146                None => {}
147                Some(result_token) => {
148                    match result_token {
149                        Result::Err(e) => {
150                            // return error
151                            return Err(format!("Error: {}", e));
152                        }
153                        Result::Ok(token) => {
154                            match token {
155                                Token::StartElement(name) => {
156                                    dom_path.push(name.to_owned());
157                                    // construct a child element and fill it (recursive)
158                                    let name = bumpalo::format!(in bump, "{}",name).into_bump_str();
159                                    let mut child_element = ElementBuilder::new(bump, name);
160                                    if name == "svg" {
161                                        // this tagname changes to svg now
162                                        html_or_svg_local = HtmlOrSvg::Svg;
163                                    }
164                                    if let HtmlOrSvg::Svg = html_or_svg_local {
165                                        // this is the
166                                        // svg elements have this namespace
167                                        child_element = child_element
168                                            .namespace(Some("http://www.w3.org/2000/svg"));
169                                    }
170                                    if name == "foreignObject" {
171                                        // this tagname changes to html for children, not for this element
172                                        html_or_svg_local = HtmlOrSvg::Html;
173                                    }
174                                    child_element = self.fill_element_builder(
175                                        reader_for_microxml,
176                                        child_element,
177                                        cx,
178                                        html_or_svg_local,
179                                        dom_path,
180                                    )?;
181                                    // if the boolean is empty or true then render the next node
182                                    if replace_boolean.unwrap_or(true) {
183                                        if let Some(repl_vec_nodes) = replace_vec_nodes {
184                                            for repl_node in repl_vec_nodes {
185                                                element = element.child(repl_node);
186                                            }
187                                            replace_vec_nodes = None;
188                                        } else {
189                                            element = element.child(child_element.finish());
190                                        }
191                                    }
192                                    if replace_boolean.is_some() {
193                                        replace_boolean = None;
194                                    }
195                                }
196                                Token::Attribute(name, value) => {
197                                    if name.starts_with("data-wt-") {
198                                        // the rest of the name does not matter,
199                                        // but it should be nice to be te name of the next attribute.
200                                        // The replace_string will always be applied to the next attribute.
201                                        let fn_name = value;
202                                        if &fn_name[..3] != "wt_" {
203                                            return Err(format!(
204                                                "{} value does not start with wt_ : {}.",
205                                                name, fn_name
206                                            ));
207                                        }
208                                        let repl_txt = self.replace_with_string(fn_name);
209                                        replace_string = Some(repl_txt);
210                                    } else if name.starts_with("data-on-") {
211                                        // it must look like data-on-click="wl_xxx" wl_ = webbrowser listener
212                                        // Only one listener for now because the api does not give me other method.
213                                        let fn_name = value.to_string();
214                                        let event_to_listen = unwrap!(name.get(8..)).to_string();
215                                        // rust_wasm_websys_utils::websysmod::debug_write(&format!("name.starts_with data-on- : .{}.{}.",&fn_name,&event_to_listen));
216                                        if !fn_name.is_empty() && &fn_name[..3] != "wl_" {
217                                            return Err(format!(
218                                                "{} value does not start with wl_ : {}.",
219                                                name, fn_name
220                                            ));
221                                        }
222
223                                        let event_to_listen =
224                                            bumpalo::format!(in bump, "{}",&event_to_listen)
225                                                .into_bump_str();
226                                        element = element
227                                            .on(event_to_listen, self.set_event_listener(fn_name));
228                                    } else {
229                                        let name =
230                                            bumpalo::format!(in bump, "{}",name).into_bump_str();
231                                        let value2;
232                                        if let Some(repl) = replace_string {
233                                            value2 =
234                                            bumpalo::format!(in bump, "{}",decode_5_xml_control_characters(&repl))
235                                                .into_bump_str();
236                                            // empty the replace_string for the next node
237                                            replace_string = None;
238                                        } else {
239                                            value2 =
240                                            bumpalo::format!(in bump, "{}",decode_5_xml_control_characters(value))
241                                                .into_bump_str();
242                                        }
243                                        element = element.attr(name, value2);
244                                    }
245                                }
246                                Token::TextNode(txt) => {
247                                    let txt2;
248                                    if let Some(repl) = replace_string {
249                                        txt2 =
250                                            bumpalo::format!(in bump, "{}",decode_5_xml_control_characters(&repl))
251                                                .into_bump_str();
252                                        // empty the replace_string for the next node
253                                        replace_string = None;
254                                    } else {
255                                        txt2 = bumpalo::format!(in bump, "{}",decode_5_xml_control_characters(txt))
256                                            .into_bump_str();
257                                    }
258                                    // here accepts only utf-8.
259                                    // rust_wasm_websys_utils::websysmod::debug_write("text node");
260                                    // rust_wasm_websys_utils::websysmod::debug_write(txt2);
261                                    // only minimum html entities are decoded
262                                    element = element.child(text(txt2));
263                                }
264                                Token::Comment(txt) => {
265                                    // the main goal of comments is to change the value of the next text node
266                                    // with the result of a function
267                                    if txt == "end_of_wt" {
268                                        // a special comment <!--end_of_wt--> just to end the wt_ replace string
269                                        // if there are more replacing inside one text node
270                                        // rust_wasm_websys_utils::websysmod::debug_write("found comment <!--end_of_wt-->");
271                                    } else if txt.starts_with("wt_") {
272                                        // it must look like <!--wt_get_text-->  wt_ = webbrowser text
273                                        let repl_txt = self.replace_with_string(txt);
274                                        replace_string = Some(repl_txt);
275                                    } else if txt.starts_with("wn_") {
276                                        // it must look like <!--wn_get_nodes-->  wn_ = webbrowser nodes
277                                        let repl_vec_nodes = self.replace_with_nodes(cx, txt);
278                                        replace_vec_nodes = Some(repl_vec_nodes);
279                                    } else if txt.starts_with("wb_") {
280                                        // it must look like <!--wb_get_bool-->  wb_ = webbrowser boolean
281                                        // boolean if this is true than render the next node, else don't render
282                                        replace_boolean =
283                                            Some(self.retain_next_node_or_attribute(txt));
284                                    } else {
285                                        // nothing. it is really a comment
286                                    }
287                                }
288                                Token::EndElement(name) => {
289                                    let last_name = unwrap!(dom_path.pop());
290                                    // it can be also auto-closing element
291                                    if last_name == name || name == "" {
292                                        return Ok(element);
293                                    } else {
294                                        return Err(format!(
295                                            "End element not correct: starts <{}> ends </{}>",
296                                            last_name, name
297                                        ));
298                                    }
299                                }
300                            }
301                        }
302                    }
303                }
304            }
305        }
306    }
307    // endregion: generic code
308}
309
310/// get en empty div node
311pub fn empty_div<'a>(cx: &mut RenderContext<'a>) -> Node<'a> {
312    let bump = cx.bump;
313    ElementBuilder::new(bump, "div").finish()
314}
315
316/// decode 5 xml control characters : " ' & < >  
317/// https://www.liquid-technologies.com/XML/EscapingData.aspx
318/// I will ignore all html entities, to keep things simple,
319/// because all others characters can be written as utf-8 characters.
320/// https://www.tutorialspoint.com/html5/html5_entities.htm  
321pub fn decode_5_xml_control_characters(input: &str) -> String {
322    // The standard library replace() function makes allocation,
323    //but is probably fast enough for my use case.
324    input
325        .replace("&quot;", "\"")
326        .replace("&apos;", "'")
327        .replace("&amp;", "&")
328        .replace("&lt;", "<")
329        .replace("&gt;", ">")
330}