htr/
lib.rs

1#![warn(missing_docs)]
2
3//! HTML to react transformer
4//!
5//! htr is a fast html to react
6//! transformer that uses btrees to search and replace
7//! html properties to the react equivalent.
8//!
9//! # How to use htr
10//!
11//! There are two ways to use htr:
12//!
13//! - **Convert props React** transform html string to a react string.
14//!   - [`convert_props_react`] is used to transform :blocking.
15//! - **Convert to React**  Lets you transform the html to a react component.
16//!   - [`convert_to_react`] is used to transform :blocking.
17//!
18//! [`convert_props_react`]: htr/fn.convert_props_react.html
19//! [`convert_to_react`]: htr/fn.convert_to_react.html
20//!
21//! # Basic usage
22//!
23//! First, you will need to add `htr` to your `Cargo.toml`.
24//!
25//! Next, add your html to one of the transform methods to get your react,
26//! output.
27
28#[macro_use]
29extern crate lazy_static;
30
31extern crate convert_case;
32
33/// transform functions
34mod transform;
35/// application utils
36mod utils;
37pub use transform::{create_style_object, extract_html_props};
38/// static map of html props
39mod statics;
40pub use statics::{HTML_PROPS, SELF_ENCLOSED_TAGS};
41
42/// convert props to react
43pub fn convert_props_react(ctx: &str) -> String {
44    let mut context = ctx.to_string();
45    let props: Vec<String> = extract_html_props(&context);
46
47    for item in props.iter() {
48        if item == "style" {
49            context = create_style_object(&context);
50        } else {
51            let value = HTML_PROPS.get(&*item.to_owned()).unwrap_or(&"");
52
53            if !value.is_empty() {
54                context = context.replace(&format!("{}=", item), &format!("{}=", value));
55            }
56        }
57    }
58
59    context
60}
61
62/// convert props to a react component
63pub fn convert_to_react(ctx: &String, component_name: String) -> String {
64    let react_html = convert_props_react(ctx);
65    let mut react_html = react_html.trim().to_owned();
66
67    // remove html tags
68    if react_html.starts_with("<!DOCTYPE html>") {
69        react_html = react_html.replace("<!DOCTYPE html>", "").trim().to_owned();
70    }
71    if react_html.starts_with("<html>") {
72        react_html = react_html.replace("<html>", "");
73        react_html = react_html.replace("</html>", "");
74    }
75    // add slow re-iterate contains [TODO get values upfront in convert_props]
76    if react_html.contains("<script")
77        || react_html.contains("<style")
78        || react_html.contains("<script>")
79        || react_html.contains("<style>")
80    {
81        react_html = convert_children_react(&mut react_html);
82    }
83
84    let component_name = format!(" {}", component_name.trim());
85
86    let component = format!(
87        r###"import React from "react"
88
89function{}() {{
90    return (
91        <>
92        {}
93        </>
94    )
95}}"###,
96        component_name, react_html
97    );
98
99    component
100}
101
102/// transform inline react children from script and style tags
103pub fn convert_children_react(ctx: &mut String) -> String {
104    let mut entry_start = false; // entry start
105    let mut entry_end = false; // end of tag
106    let mut inside_tag = false; // inside a start tag
107    let mut store_tag = false; // can store tagname
108    let mut current_prop = String::from(""); // current tagname
109
110    let mut result = String::from("");
111    let mut peekable = ctx.chars().peekable();
112
113    let mut empty_children = false; // prevent updating empty children
114
115    // TODO: capture url(base64; comma cases to build next
116    let mut block_self_enclose = false;
117
118    while let Some(c) = peekable.next() {
119        result.push(c);
120
121        // peek into next to prevent sets
122        let peeked = if c == '/' || entry_start || entry_end {
123            if let Some(cc) = peekable.peek() {
124                cc.to_string()
125            } else {
126                String::from("")
127            }
128        } else {
129            String::from("")
130        };
131
132        if c == '<' {
133            inside_tag = true;
134            store_tag = true;
135        }
136        if c == '/' && peeked == ">" {
137            block_self_enclose = true;
138        }
139        if c == '>' {
140            inside_tag = false;
141            // self enclose the tag
142            if SELF_ENCLOSED_TAGS.contains(&current_prop.trim_end().as_ref()) {
143                if !block_self_enclose {
144                    result.pop();
145                    result.push('/');
146                    result.push('>');
147                } else {
148                    block_self_enclose = false;
149                }
150            }
151        }
152
153        // entry start children append [TODO: all tags that require inlining]
154        if entry_start && c == '>' {
155            entry_start = false;
156            store_tag = true;
157
158            if peeked != "<" {
159                result.push('{');
160                result.push('`');
161                empty_children = false;
162            } else {
163                empty_children = true;
164            }
165
166            current_prop.clear();
167        }
168
169        // entry end children prepend
170        if entry_end {
171            if !empty_children {
172                for _ in 0..current_prop.len() + 1 {
173                    result.pop();
174                }
175                result.push('`');
176                result.push('}');
177                result.push_str(&format!(r#"{}>"#, current_prop));
178            } else {
179                empty_children = true;
180            }
181            entry_end = false;
182
183            current_prop.clear();
184        }
185
186        if inside_tag && store_tag {
187            current_prop.push(c);
188
189            // start of tag
190            if current_prop == "<style" || current_prop == "<script" {
191                entry_start = true;
192                empty_children = false; // reset empty children detection
193            }
194
195            // end of tag
196            if current_prop == "</style" || current_prop == "</script" {
197                entry_end = !empty_children;
198            }
199
200            // finish storing tags
201            if c == ' ' {
202                store_tag = false;
203            }
204
205            // end tag prevent store
206            if current_prop.starts_with("</") && c == '>' {
207                store_tag = false;
208            }
209        } else if !inside_tag {
210            current_prop.clear();
211        }
212    }
213
214    result
215}