1use super::attributes::AddedAttributes;
22use super::context::HtmlContext;
23use super::render::ItemRender;
24use std::collections::HashSet;
25
26macro_rules! tag_method {
27 ($tag:tt) => {
28 pub fn $tag(self) -> HtmlBuilderTag<'c, 'i, 'h, 'e, 't> {
29 self.tag(stringify!($tag))
30 }
31 };
32}
33
34const SOLO_HTML_TAGS: [&str; 14] = [
36 "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
37 "source", "track", "wbr",
38];
39
40#[derive(Debug)]
43pub struct HtmlBuilder<'c, 'i, 'h, 'e, 't>
44where
45 'e: 't,
46{
47 ctx: &'c mut HtmlContext<'i, 'h, 'e, 't>,
48}
49
50impl<'c, 'i, 'h, 'e, 't> HtmlBuilder<'c, 'i, 'h, 'e, 't>
51where
52 'e: 't,
53{
54 #[inline]
55 pub fn new(ctx: &'c mut HtmlContext<'i, 'h, 'e, 't>) -> Self {
56 HtmlBuilder { ctx }
57 }
58
59 #[inline]
61 pub fn tag(self, tag: &'t str) -> HtmlBuilderTag<'c, 'i, 'h, 'e, 't> {
62 debug_assert!(is_alphanumeric(tag));
63
64 let HtmlBuilder { ctx } = self;
65 HtmlBuilderTag::new(ctx, tag)
66 }
67
68 #[inline]
70 pub fn element(self, tag: &'t str) -> HtmlBuilderTag<'c, 'i, 'h, 'e, 't> {
71 debug_assert!(tag.starts_with("wj-"));
72
73 self.tag(tag)
74 }
75
76 #[inline]
77 pub fn table_cell(self, header: bool) -> HtmlBuilderTag<'c, 'i, 'h, 'e, 't> {
78 if header {
79 self.tag("th")
80 } else {
81 self.tag("td")
82 }
83 }
84
85 pub fn sprite(self, id: &'t str) {
87 let viewbox = match id {
88 "wj-karma" => "0 0 64 114",
89 _ => "0 0 24 24",
90 };
91
92 let class = format!("wj-sprite sprite-{id}");
93 let href = format!("/files--static/media/ui.svg#{id}");
94
95 self.tag("svg")
96 .attr(attr!(
97 "class" => &class,
98 "viewBox" => viewbox,
99 ))
100 .inner(|ctx| {
101 ctx.html().tag("use").attr(attr!("href" => &href));
102 });
103 }
104
105 tag_method!(a);
106 tag_method!(article);
107 tag_method!(br);
108 tag_method!(code);
109 tag_method!(dd);
110 tag_method!(details);
111 tag_method!(div);
112 tag_method!(dl);
113 tag_method!(dt);
114 tag_method!(em);
115 tag_method!(hr);
116 tag_method!(iframe);
117 tag_method!(img);
118 tag_method!(input);
119 tag_method!(li);
120 tag_method!(nav);
121 tag_method!(ol);
122 tag_method!(pre);
123 tag_method!(rp);
124 tag_method!(script);
125 tag_method!(style);
126 tag_method!(span);
127 tag_method!(sub);
128 tag_method!(sup);
129 tag_method!(summary);
130 tag_method!(table);
131 tag_method!(tbody);
132 tag_method!(tr);
133 tag_method!(ul);
134
135 #[inline]
136 pub fn text(&mut self, text: &str) {
137 self.ctx.push_escaped(text);
138 }
139}
140
141#[derive(Debug)]
144pub struct HtmlBuilderTag<'c, 'i, 'h, 'e, 't>
145where
146 'e: 't,
147{
148 ctx: &'c mut HtmlContext<'i, 'h, 'e, 't>,
149 tag: &'t str,
150 in_tag: bool,
151 in_contents: bool,
152}
153
154impl<'c, 'i, 'h, 'e, 't> HtmlBuilderTag<'c, 'i, 'h, 'e, 't> {
155 pub fn new(ctx: &'c mut HtmlContext<'i, 'h, 'e, 't>, tag: &'t str) -> Self {
156 ctx.push_raw('<');
157 ctx.push_raw_str(tag);
158
159 HtmlBuilderTag {
160 ctx,
161 tag,
162 in_tag: true,
163 in_contents: false,
164 }
165 }
166
167 fn attr_key(&mut self, key: &str, has_value: bool) {
168 debug_assert!(is_alphanumeric(key));
169 debug_assert!(self.in_tag);
170
171 self.ctx.push_raw(' ');
172 self.ctx.push_escaped(key);
173
174 if has_value {
175 self.ctx.push_raw('=');
176 }
177 }
178
179 fn attr_value(&mut self, value_parts: &[&str]) {
180 self.ctx.push_raw('"');
181
182 for part in value_parts {
183 self.ctx.push_escaped(part);
184 }
185
186 self.ctx.push_raw('"');
187 }
188
189 pub fn attr_single(&mut self, key: &str, value_parts: &[&str]) -> &mut Self {
190 let has_value = !value_parts.iter().all(|s| s.is_empty());
202
203 self.attr_key(key, has_value);
204
205 if has_value {
206 self.attr_value(value_parts);
207 }
208
209 self
210 }
211
212 pub fn attr(&mut self, attributes: AddedAttributes) -> &mut Self {
213 fn filter_entries<'a>(
214 attributes: &AddedAttributes<'a>,
215 ) -> impl Iterator<Item = (&'a str, &'a [&'a str])> {
216 attributes.entries.iter().filter_map(
217 |(item, accept)| {
218 if *accept { Some(*item) } else { None }
219 },
220 )
221 }
222
223 let mut merged = HashSet::new();
224 let mut merged_value = Vec::new();
225
226 if let Some(attribute_map) = attributes.map {
228 let attribute_map = attribute_map.get();
229
230 for (key, value_parts) in filter_entries(&attributes) {
231 if let Some(map_value) = attribute_map.get(&cow!(key)) {
232 merged_value.clear();
236 merged_value.extend(value_parts);
237 merged_value.push(" ");
238 merged_value.push(map_value);
239
240 self.attr_single(key, &merged_value);
241 merged.insert(key);
242 }
243 }
244 }
245
246 for (key, value_parts) in filter_entries(&attributes) {
248 if !merged.contains(key) {
249 self.attr_single(key, value_parts);
250 }
251 }
252
253 if let Some(attribute_map) = attributes.map {
255 for (key, value) in attribute_map.get() {
256 if !merged.contains(key.as_ref()) {
257 self.attr_single(key, &[value]);
258 }
259 }
260 }
261
262 self
263 }
264
265 fn content_start(&mut self) {
266 if self.in_tag {
267 self.ctx.push_raw('>');
268 self.in_tag = false;
269 }
270
271 assert!(!self.in_contents, "Already in tag contents");
272 self.in_contents = true;
273 }
274
275 #[inline]
276 pub fn contents<R: ItemRender>(&mut self, item: R) -> &mut Self {
277 self.content_start();
278 item.render(self.ctx);
279 self
280 }
281
282 pub fn inner<F>(&mut self, mut f: F) -> &mut Self
283 where
284 F: FnMut(&mut HtmlContext),
285 {
286 self.content_start();
287 f(self.ctx);
288 self
289 }
290}
291
292impl Drop for HtmlBuilderTag<'_, '_, '_, '_, '_> {
293 fn drop(&mut self) {
294 if self.in_tag && !self.in_contents {
295 self.ctx.push_raw('>');
296 }
297
298 if should_close_tag(self.tag) {
299 self.ctx.push_raw_str("</");
300 self.ctx.push_raw_str(self.tag);
301 self.ctx.push_raw('>');
302 }
303 }
304}
305
306fn is_alphanumeric(value: &str) -> bool {
309 value
310 .chars()
311 .all(|c| c.is_ascii_alphabetic() || c.is_ascii_digit() || c == '-')
312}
313
314#[inline]
315fn should_close_tag(tag: &str) -> bool {
316 !SOLO_HTML_TAGS.contains(&tag)
317}