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(""", "\"")
326 .replace("'", "'")
327 .replace("&", "&")
328 .replace("<", "<")
329 .replace(">", ">")
330}