dioxus_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    Attribute, AttributeName, AttributeValue, BodyNode, CallBody, Component, Element, ElementName,
9    HotLiteral, TemplateBody, TextNode,
10};
11pub use html_parser::{Dom, Node};
12use htmlentity::entity::ICodedDataTrait;
13use proc_macro2::{Ident, Span};
14use syn::{punctuated::Punctuated, LitStr};
15
16/// Convert an HTML DOM tree into an RSX CallBody
17pub fn rsx_from_html(dom: &Dom) -> CallBody {
18    let nodes = dom
19        .children
20        .iter()
21        .filter_map(rsx_node_from_html)
22        .collect::<Vec<_>>();
23
24    let template = TemplateBody::new(nodes);
25
26    CallBody::new(template)
27}
28
29/// Convert an HTML Node into an RSX BodyNode
30///
31/// If the node is a comment, it will be ignored since RSX doesn't support comments
32pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
33    use AttributeName::*;
34    use AttributeValue::*;
35
36    match node {
37        Node::Text(text) => Some(BodyNode::Text(TextNode::from_text(
38            &htmlentity::entity::decode(text.as_bytes())
39                .to_string()
40                .ok()?,
41        ))),
42
43        Node::Element(el) => {
44            let el_name = if let Some(name) = map_html_element_to_rsx(&el.name) {
45                ElementName::Ident(Ident::new(name, Span::call_site()))
46            } else {
47                // if we don't recognize it and it has a dash, we assume it's a web component
48                if el.name.contains('-') {
49                    ElementName::Custom(LitStr::new(&el.name, Span::call_site()))
50                } else {
51                    // otherwise, it might be an element that isn't supported yet
52                    ElementName::Ident(Ident::new(&el.name.to_case(Case::Snake), Span::call_site()))
53                }
54            };
55
56            let mut attributes: Vec<_> = el
57                .attributes
58                .iter()
59                .map(|(name, value)| {
60                    // xlink attributes are deprecated and technically we can't handle them.
61                    // todo(jon): apply the namespaces to the attributes
62                    let (_namespace, name) = name.split_once(':').unwrap_or(("", name));
63
64                    let value = HotLiteral::from_raw_text(value.as_deref().unwrap_or("false"));
65                    let attr = if let Some(name) = map_html_attribute_to_rsx(name) {
66                        let name = if let Some(name) = name.strip_prefix("r#") {
67                            Ident::new_raw(name, Span::call_site())
68                        } else {
69                            Ident::new(name, Span::call_site())
70                        };
71                        BuiltIn(name)
72                    } else {
73                        // If we don't recognize the attribute, we assume it's a custom attribute
74                        Custom(LitStr::new(name, Span::call_site()))
75                    };
76
77                    Attribute::from_raw(attr, AttrLiteral(value))
78                })
79                .collect();
80
81            let class = el.classes.join(" ");
82            if !class.is_empty() {
83                attributes.push(Attribute::from_raw(
84                    BuiltIn(Ident::new("class", Span::call_site())),
85                    AttrLiteral(HotLiteral::from_raw_text(&class)),
86                ));
87            }
88
89            if let Some(id) = &el.id {
90                attributes.push(Attribute::from_raw(
91                    BuiltIn(Ident::new("id", Span::call_site())),
92                    AttrLiteral(HotLiteral::from_raw_text(id)),
93                ));
94            }
95
96            // the html-parser crate we use uses a HashMap for attributes. This leads to a
97            // non-deterministic order of attributes.
98            // Sort them here
99            attributes.sort_by(|a, b| a.name.to_string().cmp(&b.name.to_string()));
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                raw_attributes: attributes,
107                merged_attributes: Default::default(),
108                diagnostics: Default::default(),
109                spreads: Default::default(),
110                brace: Default::default(),
111            }))
112        }
113
114        // We ignore comments
115        Node::Comment(_) => None,
116    }
117}
118
119/// Pull out all the svgs from the body and replace them with components of the same name
120pub fn collect_svgs(children: &mut [BodyNode], out: &mut Vec<BodyNode>) {
121    for child in children {
122        match child {
123            BodyNode::Component(comp) => collect_svgs(&mut comp.children.roots, out),
124
125            BodyNode::Element(el) if el.name == "svg" => {
126                // we want to replace this instance with a component
127                let mut segments = Punctuated::new();
128
129                segments.push(Ident::new("icons", Span::call_site()).into());
130
131                let new_name: Ident = Ident::new(&format!("icon_{}", out.len()), Span::call_site());
132
133                segments.push(new_name.clone().into());
134
135                // Replace this instance with a component
136                let mut new_comp = BodyNode::Component(Component {
137                    name: syn::Path {
138                        leading_colon: None,
139                        segments,
140                    },
141                    generics: None,
142                    spreads: Default::default(),
143                    diagnostics: Default::default(),
144                    fields: vec![],
145                    children: TemplateBody::new(vec![]),
146                    brace: Some(Default::default()),
147                    dyn_idx: Default::default(),
148                    component_literal_dyn_idx: vec![],
149                });
150
151                std::mem::swap(child, &mut new_comp);
152
153                // And push the original svg into the svg list
154                out.push(new_comp);
155            }
156
157            BodyNode::Element(el) => collect_svgs(&mut el.children, out),
158
159            _ => {}
160        }
161    }
162}