1use core::fmt;
4use core::mem::take;
5
6use super::html::Html;
7use super::tag::{Tag, TagType};
8use crate::errors::{safe_expect, safe_unreachable};
9
10#[derive(Debug)]
12pub struct CommentFull(bool);
13
14#[non_exhaustive]
19#[derive(Debug, Default)]
20pub enum HtmlBuilder {
21 #[non_exhaustive]
27 Comment {
28 content: String,
34 full: CommentFull,
42 },
43 #[non_exhaustive]
51 Doctype {
52 name: String,
58 attr: Option<String>,
64 },
65 #[default]
69 Empty,
70 #[non_exhaustive]
78 Tag {
79 tag: Tag,
83 full: TagType,
88 child: Box<HtmlBuilder>,
96 },
97 Text(String),
105 Vec(Vec<HtmlBuilder>),
112}
113
114impl HtmlBuilder {
115 pub fn close_comment(&mut self) -> bool {
117 match self {
118 Self::Comment { full, .. } =>
119 if full.0 {
120 false
121 } else {
122 full.0 = true;
123 true
124 },
125 Self::Text(_) | Self::Empty | Self::Doctype { .. } => false,
126 Self::Tag { full, child, .. } => full.is_open() && child.close_comment(),
127 Self::Vec(vec) =>
128 safe_expect!(vec.last_mut(), "Html vec built with one.").close_comment(),
129 }
130 }
131
132 pub fn close_tag(&mut self, name: &str) -> Result<(), String> {
136 if self.close_tag_aux(name) {
137 Ok(())
138 } else {
139 Err(format!(
140 "Invalid closing tag: Found closing tag for '{name}' but it isn't open."
141 ))
142 }
143 }
144
145 pub fn close_tag_aux(&mut self, name: &str) -> bool {
151 if let Self::Tag { tag, full: full @ TagType::Opened, child } = self {
152 child.close_tag_aux(name)
153 || (tag.as_name() == name && {
154 *full = TagType::Closed;
155 true
156 })
157 } else if let Self::Vec(vec) = self {
158 vec.last_mut()
159 .is_some_and(|child| child.close_tag_aux(name))
160 } else {
161 false
162 }
163 }
164
165 pub fn empty_box() -> Box<Self> {
167 Box::new(Self::default())
168 }
169
170 pub fn from_char(ch: char) -> Self {
172 Self::Text(ch.to_string())
173 }
174
175 pub fn into_html(self) -> Html {
177 match self {
178 Self::Comment { content, .. } => Html::Comment(content),
179 Self::Doctype { name, attr } => Html::Doctype { name, attr },
180 Self::Empty => Html::Empty,
181 Self::Tag { tag, child, .. } => Html::Tag { tag, child: Box::new(child.into_html()) },
182 Self::Text(text) => Html::Text(text),
183 Self::Vec(vec) => Html::Vec(vec.into_iter().map(Self::into_html).collect()),
184 }
185 }
186
187 #[coverage(off)]
193 pub fn is_pushable(&self, is_char: bool) -> bool {
194 match self {
195 Self::Empty | Self::Vec(_) => safe_unreachable("Vec or Empty can't be in vec"),
196 Self::Tag { full, .. } => full.is_open(),
197 Self::Doctype { .. } => false,
198 Self::Text(_) => is_char,
199 Self::Comment { full, .. } => !full.0,
200 }
201 }
202
203 pub fn push_char(&mut self, ch: char) {
205 match self {
206 Self::Empty => *self = Self::from_char(ch),
207 Self::Tag { child, full: TagType::Opened, .. } => child.push_char(ch),
208 Self::Doctype { .. }
209 | Self::Tag { full: TagType::Closed | TagType::SelfClosing, .. } =>
210 *self = Self::Vec(vec![take(self), Self::from_char(ch)]),
211 Self::Text(text) => text.push(ch),
212 Self::Vec(vec) => {
213 let last = safe_expect!(vec.last_mut(), "Initialised with one element.");
214 if last.is_pushable(true) {
215 return last.push_char(ch);
216 }
217 vec.push(Self::from_char(ch));
218 }
219 Self::Comment { content, full } => {
220 if full.0 {
221 *self = Self::Vec(vec![take(self), Self::from_char(ch)]);
223 } else {
224 content.push(ch);
225 }
226 }
227 }
228 }
229
230 pub fn push_comment(&mut self) {
232 self.push_node(Self::Comment { content: String::new(), full: CommentFull(false) });
233 }
234
235 #[coverage(off)]
239 pub fn push_node(&mut self, node: Self) {
240 match self {
241 Self::Empty => *self = node,
242 Self::Tag { child, full: TagType::Opened, .. } => child.push_node(node),
243 Self::Text(_)
244 | Self::Doctype { .. }
245 | Self::Tag { full: TagType::Closed | TagType::SelfClosing, .. } =>
246 *self = Self::Vec(vec![take(self), node]),
247 Self::Vec(vec) => {
248 let last = safe_expect!(vec.last_mut(), "Initialised with one element.");
249 if last.is_pushable(false) {
250 return last.push_node(node);
251 }
252 vec.push(node);
253 }
254 Self::Comment { .. } => safe_unreachable("Pushed parsed not into an unclosed comment."),
255 }
256 }
257
258 pub fn push_tag(&mut self, tag: Tag, inline: bool) {
260 self.push_node(Self::Tag {
261 tag,
262 full: if inline {
263 TagType::SelfClosing
264 } else {
265 TagType::Opened
266 },
267 child: Self::empty_box(),
268 });
269 }
270}
271
272#[coverage(off)]
273#[expect(clippy::min_ident_chars, reason = "keep trait naming")]
274impl fmt::Display for HtmlBuilder {
275 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276 match self {
277 Self::Empty => "".fmt(f),
278 Self::Tag { tag, full, child } => match full {
279 TagType::Closed => write!(f, "<{tag}>{child}</{}>", tag.as_name()),
280 TagType::Opened => write!(f, "<{tag}>{child}"),
281 TagType::SelfClosing => write!(f, "<{tag} />"),
282 },
283 Self::Doctype { name, attr } => match (name, attr) {
284 (name_str, Some(attr_str)) => write!(f, "<!{name_str} {attr_str}>"),
285 (name_str, None) if name_str.is_empty() => write!(f, "<!>"),
286 (name_str, None) => write!(f, "<!{name_str} >"),
287 },
288 Self::Text(text) => text.fmt(f),
289 Self::Vec(vec) => vec.iter().try_for_each(|html| html.fmt(f)),
290 Self::Comment { content, full } => f
291 .write_str("<!--")
292 .and_then(|()| f.write_str(content))
293 .and_then(|()| if full.0 { f.write_str("-->") } else { Ok(()) }),
294 }
295 }
296}