rsx_rosetta/
lib.rs

1#![doc = include_str!("../README.md")]
2#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
3#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
4
5use convert_case::{Case, Casing};
6use dioxus_html::{map_html_attribute_to_rsx, map_html_element_to_rsx};
7use dioxus_rsx::{
8    AttributeType, BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed,
9    ElementName, IfmtInput,
10};
11pub use html_parser::{Dom, Node};
12use proc_macro2::{Ident, Span};
13use syn::{punctuated::Punctuated, LitStr};
14
15/// Convert an HTML DOM tree into an RSX CallBody
16pub fn rsx_from_html(dom: &Dom) -> CallBody {
17    CallBody {
18        roots: dom.children.iter().filter_map(rsx_node_from_html).collect(),
19    }
20}
21
22/// Convert an HTML Node into an RSX BodyNode
23///
24/// If the node is a comment, it will be ignored since RSX doesn't support comments
25pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
26    match node {
27        Node::Text(text) => Some(BodyNode::Text(ifmt_from_text(text))),
28        Node::Element(el) => {
29            let el_name = if let Some(name) = map_html_element_to_rsx(&el.name) {
30                ElementName::Ident(Ident::new(name, Span::call_site()))
31            } else {
32                // if we don't recognize it and it has a dash, we assume it's a web component
33                if el.name.contains('-') {
34                    ElementName::Custom(LitStr::new(&el.name, Span::call_site()))
35                } else {
36                    // otherwise, it might be an element that isn't supported yet
37                    ElementName::Ident(Ident::new(&el.name.to_case(Case::Snake), Span::call_site()))
38                }
39            };
40
41            let mut attributes: Vec<_> = el
42                .attributes
43                .iter()
44                .map(|(name, value)| {
45                    let value = ifmt_from_text(value.as_deref().unwrap_or("false"));
46                    let attr = if let Some(name) = map_html_attribute_to_rsx(name) {
47                        let ident = if let Some(name) = name.strip_prefix("r#") {
48                            Ident::new_raw(name, Span::call_site())
49                        } else {
50                            Ident::new(name, Span::call_site())
51                        };
52                        ElementAttr {
53                            value: dioxus_rsx::ElementAttrValue::AttrLiteral(value),
54                            name: dioxus_rsx::ElementAttrName::BuiltIn(ident),
55                        }
56                    } else {
57                        // If we don't recognize the attribute, we assume it's a custom attribute
58                        ElementAttr {
59                            value: dioxus_rsx::ElementAttrValue::AttrLiteral(value),
60                            name: dioxus_rsx::ElementAttrName::Custom(LitStr::new(
61                                name,
62                                Span::call_site(),
63                            )),
64                        }
65                    };
66
67                    AttributeType::Named(ElementAttrNamed {
68                        el_name: el_name.clone(),
69                        attr,
70                    })
71                })
72                .collect();
73
74            let class = el.classes.join(" ");
75            if !class.is_empty() {
76                attributes.push(AttributeType::Named(ElementAttrNamed {
77                    el_name: el_name.clone(),
78                    attr: ElementAttr {
79                        name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new(
80                            "class",
81                            Span::call_site(),
82                        )),
83                        value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(&class)),
84                    },
85                }));
86            }
87
88            if let Some(id) = &el.id {
89                attributes.push(AttributeType::Named(ElementAttrNamed {
90                    el_name: el_name.clone(),
91                    attr: ElementAttr {
92                        name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new(
93                            "id",
94                            Span::call_site(),
95                        )),
96                        value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(id)),
97                    },
98                }));
99            }
100
101            let children = el.children.iter().filter_map(rsx_node_from_html).collect();
102
103            Some(BodyNode::Element(Element {
104                name: el_name,
105                children,
106                attributes,
107                merged_attributes: Default::default(),
108                key: None,
109                brace: Default::default(),
110            }))
111        }
112
113        // We ignore comments
114        Node::Comment(_) => None,
115    }
116}
117
118/// Pull out all the svgs from the body and replace them with components of the same name
119pub fn collect_svgs(children: &mut [BodyNode], out: &mut Vec<BodyNode>) {
120    for child in children {
121        match child {
122            BodyNode::Component(comp) => collect_svgs(&mut comp.children, out),
123
124            BodyNode::Element(el) if el.name == "svg" => {
125                // we want to replace this instance with a component
126                let mut segments = Punctuated::new();
127
128                segments.push(Ident::new("icons", Span::call_site()).into());
129
130                let new_name: Ident = Ident::new(&format!("icon_{}", out.len()), Span::call_site());
131
132                segments.push(new_name.clone().into());
133
134                // Replace this instance with a component
135                let mut new_comp = BodyNode::Component(Component {
136                    name: syn::Path {
137                        leading_colon: None,
138                        segments,
139                    },
140                    prop_gen_args: None,
141                    fields: vec![],
142                    children: vec![],
143                    manual_props: None,
144                    key: None,
145                    brace: Default::default(),
146                    location: Default::default(),
147                });
148
149                std::mem::swap(child, &mut new_comp);
150
151                // And push the original svg into the svg list
152                out.push(new_comp);
153            }
154
155            BodyNode::Element(el) => collect_svgs(&mut el.children, out),
156
157            _ => {}
158        }
159    }
160}
161
162fn ifmt_from_text(text: &str) -> IfmtInput {
163    IfmtInput {
164        source: Some(LitStr::new(text, Span::call_site())),
165        segments: vec![],
166    }
167}