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}