next_rs/
head.rs

1use crate::prelude::*;
2use std::collections::{HashMap, HashSet};
3use web_sys::window;
4use yew::virtual_dom::VTag;
5
6// Define METATYPES as a static array
7static METATYPES: [&'static str; 5] = ["name", "httpEquiv", "charSet", "itemProp", "property"];
8
9// MetaCategories type to store meta category information
10type MetaCategories = HashMap<&'static str, HashSet<String>>;
11
12/// Generates the default `<head>` element with a charset meta tag.
13///
14/// # Example
15/// ```rust
16/// use next_rs::prelude::*;
17/// use next_rs::head::default_head;
18///
19/// #[func]
20/// pub fn MyComponent() -> Html {
21///
22///     rsx! {
23///         <>{default_head()}</>
24///     }
25/// }
26/// ```
27pub fn default_head() -> Html {
28    rsx! { <meta charset="utf-8" /> }
29}
30
31/// Reduces a vector of HTML components by flattening and filtering out duplicates.
32///
33/// # Example
34/// ```rust
35/// use next_rs::head::{map_components, default_head};
36///
37/// let components = vec![default_head()];
38/// let new_components = map_components(components);
39/// ```
40pub fn map_components(components: Vec<Html>) -> Vec<Html> {
41    let flattened: Vec<Html> = components
42        .into_iter()
43        .flat_map(|c| match c {
44            Html::VTag(tag) => tag.children().into_iter().cloned().collect::<Vec<_>>(),
45            _ => vec![],
46        })
47        .collect();
48
49    let filtered: Vec<Html> = flattened.into_iter().filter(unique).collect();
50
51    let mut head = vec![default_head()];
52
53    for child in filtered.clone() {
54        match child {
55            Html::VTag(_tag) => {
56                // TODO
57            }
58            Html::VText(text) => {
59                // Hack: Handle VText case, like title tag
60                let text_str = text.text;
61                let mut tag = VTag::new("title");
62                tag.add_child(text_str.into());
63                head.push(tag.into());
64            }
65            Html::VComp(_component) => {
66                // TODO
67            }
68            _ => {}
69        }
70    }
71
72    // add next-rs trademark, rn
73    let final_result: Vec<Html> = head
74        .into_iter()
75        .map(|c| match c {
76            Html::VTag(mut tag) => {
77                let class_name = format!(
78                    "{} {}",
79                    "next-rs-tag",
80                    tag.attributes
81                        .iter()
82                        .find(|(key, _)| *key == "class")
83                        .map(|(_, value)| value)
84                        .unwrap_or_default()
85                );
86                tag.add_attribute("class", class_name);
87                Html::VTag(tag)
88            }
89            _ => c,
90        })
91        .collect();
92
93    final_result
94}
95
96/// Returns a function for filtering head child elements which shouldn't be duplicated, like <title/>.
97pub fn unique(head: &Html) -> bool {
98    match head {
99        Html::VTag(tag) => match tag.tag() {
100            "title" | "base" => tag.key.is_some(),
101            "meta" => {
102                for metatype in METATYPES.iter() {
103                    if !tag
104                        .attributes
105                        .iter()
106                        .find(|(key, _)| *key == *metatype)
107                        .map(|(_, value)| value)
108                        .unwrap_or_default()
109                        .is_empty()
110                    {
111                        match *metatype {
112                            "charSet" => {
113                                if !tag
114                                    .attributes
115                                    .iter()
116                                    .find(|(key, _)| *key == "charSet")
117                                    .map(|(_, value)| value)
118                                    .unwrap_or_default()
119                                    .is_empty()
120                                {
121                                    return false;
122                                }
123                            }
124                            _ => {
125                                let category = tag
126                                    .attributes
127                                    .iter()
128                                    .find(|(key, _)| *key == *metatype)
129                                    .map(|(_, value)| value)
130                                    .unwrap_or_default();
131                                let mut meta_categories = MetaCategories::new();
132                                let categories = meta_categories
133                                    .entry(metatype)
134                                    .or_insert_with(|| HashSet::new());
135                                if categories.contains(&category.to_string()) {
136                                    return false;
137                                }
138
139                                categories.insert(category.to_string());
140                            }
141                        }
142                    }
143                }
144                true
145            }
146            _ => true,
147        },
148        _ => true,
149    }
150}
151
152// Define the HeadProps struct
153#[derive(Properties, Clone, PartialEq)]
154pub struct HeadProps {
155    pub children: Html,
156}
157
158/// A component representing the `<head>` element.
159///
160/// # Example
161/// ```rust
162/// use next_rs::head::Head;
163/// use next_rs::prelude::*;
164///
165/// #[func]
166/// pub fn MyComponent() -> Html {
167///
168///     rsx! {
169///         <Head>
170///             <title>{"Next RS Title"}</title>
171///         </Head>
172///     }
173/// }
174/// ```
175#[func]
176pub fn Head(props: &HeadProps) -> Html {
177    let state: Vec<Html> = map_components(vec![props.children.clone()]);
178
179    let document = window().and_then(|win| win.document()).unwrap();
180    let head = document.head().expect("Failed to get head element");
181
182    create_portal(rsx! {<>{ for state.into_iter() }</> }, head.clone().into())
183}