alpine_markup/
lib.rs

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}