1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::fmt::{self, Display, Formatter};
4
5pub enum Node {
6 Text(String),
7 Element(Element),
8}
9
10impl Display for Node {
11 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
12 match self {
13 Self::Text(text) => write!(f, "{text}"),
14 Self::Element(element) => write!(f, "{element}"),
15 }
16 }
17}
18
19impl From<String> for Node {
20 fn from(text: String) -> Self {
21 Self::Text(text)
22 }
23}
24
25impl From<&str> for Node {
26 fn from(text: &str) -> Self {
27 text.to_owned().into()
28 }
29}
30
31impl From<Element> for Node {
32 fn from(element: Element) -> Self {
33 Self::Element(element)
34 }
35}
36
37pub struct Element {
38 name: &'static str,
39 classes: Vec<Cow<'static, str>>,
40 attributes: Option<BTreeMap<Cow<'static, str>, String>>,
41 children: Vec<Node>,
42}
43
44impl Display for Element {
45 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
46 write!(f, "<{}", self.name)?;
47 if !self.classes.is_empty() {
48 write!(f, " class=\"{}\"", self.classes.join(" "))?;
49 }
50 if let Some(attributes) = &self.attributes {
51 for (key, value) in attributes {
52 write!(f, " {key}=\"{value}\"")?;
53 }
54 }
55 if self.children.is_empty() {
56 write!(f, " />")
57 } else {
58 write!(f, ">")?;
59 for child in &self.children {
60 write!(f, "{child}",)?;
61 }
62 write!(f, "</{}>", self.name)
63 }
64 }
65}
66
67macro_rules! attributes {
68 ($($name:ident)+) => {$(
69 #[must_use]
70 pub fn $name(self, value: impl ToString) -> Self {
71 self.attr(stringify!($name), value.to_string())
72 }
73 )+};
74}
75
76impl Element {
77 #[must_use]
78 pub const fn new(name: &'static str) -> Self {
79 Self {
80 name,
81 attributes: None,
82 classes: Vec::new(),
83 children: Vec::new(),
84 }
85 }
86
87 #[must_use]
88 pub fn attr(mut self, key: impl Into<Cow<'static, str>>, value: impl ToString) -> Self {
89 let key = key.into();
90 let value = value.to_string();
91 if key == "class" {
92 for class in value.split(' ') {
93 self.classes.push(class.to_string().into());
94 }
95 } else {
96 self.attributes
97 .get_or_insert_with(BTreeMap::new)
98 .insert(key, value.to_string());
99 }
100 self
101 }
102
103 #[must_use]
104 pub fn data(self, key: &'static str, value: impl ToString) -> Self {
105 self.attr(format!("data-{key}"), value)
106 }
107
108 #[must_use]
109 pub fn classes(self, classes: impl IntoIterator<Item = impl ToString>) -> Self {
110 classes.into_iter().fold(self, Self::class)
111 }
112
113 #[must_use]
114 pub fn classes_if(
115 self,
116 condition: bool,
117 if_true: impl IntoIterator<Item = impl ToString>,
118 ) -> Self {
119 if condition {
120 self.classes(if_true)
121 } else {
122 self
123 }
124 }
125
126 #[must_use]
127 pub fn classes_if_else(
128 self,
129 condition: bool,
130 if_true: impl IntoIterator<Item = impl ToString>,
131 if_false: impl IntoIterator<Item = impl ToString>,
132 ) -> Self {
133 if condition {
134 self.classes(if_true)
135 } else {
136 self.classes(if_false)
137 }
138 }
139
140 #[must_use]
141 pub fn add_class(self, class: impl ToString) -> Self {
142 self.attr("class", class)
143 }
144
145 attributes! {
146 charset
147 class
148 height
149 href
150 id
151 src
152 width
153 name
154 value
155 content
156 }
157
158 #[must_use]
159 pub fn push(mut self, child: impl Markup) -> Self {
160 self.children.extend(child.iter_nodes().map(Into::into));
161 self
162 }
163
164 #[must_use]
165 pub fn push_map<I, T, F, M>(mut self, items: I, func: F) -> Self
166 where
167 I: IntoIterator<Item = T>,
168 F: Fn(T) -> M,
169 M: Markup,
170 {
171 self.children.extend(
172 items
173 .into_iter()
174 .map(func)
175 .flat_map(Markup::iter_nodes)
176 .map(Into::into),
177 );
178 self
179 }
180
181 #[must_use]
182 pub fn push_if(mut self, condition: bool, child: impl Markup) -> Self {
183 if condition {
184 self.children.extend(child.iter_nodes().map(Into::into));
185 }
186 self
187 }
188
189 #[must_use]
190 pub fn push_if_else(
191 mut self,
192 condition: bool,
193 if_true: impl Markup,
194 if_false: impl Markup,
195 ) -> Self {
196 if condition {
197 self.children.extend(if_true.iter_nodes().map(Into::into));
198 } else {
199 self.children.extend(if_false.iter_nodes().map(Into::into));
200 }
201 self
202 }
203
204 #[must_use]
205 pub fn push_if_some<T, F, M>(self, maybe_t: Option<T>, func: F) -> Self
206 where
207 F: Fn(T) -> M,
208 M: Markup,
209 {
210 if let Some(t) = maybe_t {
211 self.push(func(t))
212 } else {
213 self
214 }
215 }
216
217 #[must_use]
218 pub fn tag(&self) -> &'static str {
219 self.name
220 }
221}
222
223pub trait Markup {
224 type Iter: Iterator<Item = Self::Item>;
225 type Item: Into<Node>;
226 fn iter_nodes(self) -> Self::Iter;
227}
228
229impl<T> Markup for T
230where
231 T: Into<Node>,
232{
233 type Iter = std::iter::Once<Self::Item>;
234 type Item = Self;
235 fn iter_nodes(self) -> Self::Iter {
236 std::iter::once(self)
237 }
238}
239
240impl<const N: usize> Markup for [Element; N] {
241 type Iter = std::array::IntoIter<Self::Item, N>;
242 type Item = Element;
243 fn iter_nodes(self) -> Self::Iter {
244 self.into_iter()
245 }
246}
247
248impl Markup for Vec<Element> {
249 type Iter = std::vec::IntoIter<Element>;
250 type Item = Element;
251 fn iter_nodes(self) -> Self::Iter {
252 self.into_iter()
253 }
254}
255
256#[macro_export]
257macro_rules! raw_tags {
258 ($($upper_name:ident, $lower_name:literal)+) => {$(
259 pub const $upper_name: $crate::Element = $crate::Element::new($lower_name);
260 )+};
261}