1use std::borrow::Cow;
2
3use topcoat_core::context::Cx;
4
5use crate::{
6 Captured, ClassViewParts, HtmlContext, PartsWriter, PromotedStr, StaticStr, Unescaped,
7};
8
9pub trait AttributeValueViewParts {
32 fn attribute_present(&self) -> bool;
38
39 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>);
41}
42
43macro_rules! impl_primitive {
44 ($ty:ty, $method:ident) => {
45 impl AttributeValueViewParts for $ty {
46 #[inline]
47 fn attribute_present(&self) -> bool {
48 true
49 }
50
51 #[inline]
52 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
53 parts.$method(self);
54 }
55 }
56 };
57 ($ty:ty, $method:ident, ref) => {
58 impl_primitive!($ty, $method);
59
60 impl AttributeValueViewParts for &$ty {
61 #[inline]
62 fn attribute_present(&self) -> bool {
63 (*self).attribute_present()
64 }
65
66 #[inline]
67 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
68 (*self).into_view_parts(cx, parts);
69 }
70 }
71 };
72}
73
74impl_primitive!(char, push_char, ref);
75impl_primitive!(i8, push_i8, ref);
76impl_primitive!(i16, push_i16, ref);
77impl_primitive!(i32, push_i32, ref);
78impl_primitive!(i64, push_i64, ref);
79impl_primitive!(i128, push_i128, ref);
80impl_primitive!(isize, push_isize, ref);
81impl_primitive!(u8, push_u8, ref);
82impl_primitive!(u16, push_u16, ref);
83impl_primitive!(u32, push_u32, ref);
84impl_primitive!(u64, push_u64, ref);
85impl_primitive!(u128, push_u128, ref);
86impl_primitive!(usize, push_usize, ref);
87impl_primitive!(f32, push_f32, ref);
88impl_primitive!(f64, push_f64, ref);
89impl_primitive!(String, push_string);
90
91impl AttributeValueViewParts for Cow<'static, str> {
92 #[inline]
93 fn attribute_present(&self) -> bool {
94 true
95 }
96
97 #[inline]
98 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
99 match self {
100 Cow::Borrowed(value) => parts.push_static_str(value),
101 Cow::Owned(value) => parts.push_string(value),
102 };
103 }
104}
105
106impl AttributeValueViewParts for &str {
107 #[inline]
108 fn attribute_present(&self) -> bool {
109 true
110 }
111
112 #[inline]
113 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
114 parts.push_str(self);
115 }
116}
117
118impl AttributeValueViewParts for PromotedStr {
119 #[inline]
120 fn attribute_present(&self) -> bool {
121 true
122 }
123
124 #[inline]
125 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
126 parts.push_promoted_str(self.0);
127 }
128}
129
130impl AttributeValueViewParts for StaticStr {
131 #[inline]
132 fn attribute_present(&self) -> bool {
133 true
134 }
135
136 #[inline]
137 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
138 parts.push_static_str(self.0);
139 }
140}
141
142impl AttributeValueViewParts for Unescaped<String> {
143 #[inline]
144 fn attribute_present(&self) -> bool {
145 true
146 }
147
148 #[inline]
149 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
150 parts.push_string_unescaped(self.0);
151 }
152}
153
154impl AttributeValueViewParts for Unescaped<&'static str> {
155 #[inline]
156 fn attribute_present(&self) -> bool {
157 true
158 }
159
160 #[inline]
161 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
162 parts.push_static_str_unescaped(self.0);
163 }
164}
165
166impl AttributeValueViewParts for Unescaped<PromotedStr> {
167 #[inline]
168 fn attribute_present(&self) -> bool {
169 true
170 }
171
172 #[inline]
173 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
174 parts.push_promoted_str_unescaped(self.0.0);
175 }
176}
177
178impl AttributeValueViewParts for Unescaped<StaticStr> {
179 #[inline]
180 fn attribute_present(&self) -> bool {
181 true
182 }
183
184 #[inline]
185 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
186 parts.push_static_str_unescaped(self.0.0);
187 }
188}
189
190impl AttributeValueViewParts for &String {
191 #[inline]
192 fn attribute_present(&self) -> bool {
193 self.as_str().attribute_present()
194 }
195
196 #[inline]
197 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
198 AttributeValueViewParts::into_view_parts(self.as_str(), cx, parts);
199 }
200}
201
202impl AttributeValueViewParts for bool {
203 #[inline]
204 fn attribute_present(&self) -> bool {
205 *self
206 }
207
208 #[inline]
209 fn into_view_parts(self, _cx: &Cx, _parts: &mut PartsWriter<'_>) {
210 }
213}
214
215impl AttributeValueViewParts for &bool {
216 #[inline]
217 fn attribute_present(&self) -> bool {
218 (*self).attribute_present()
219 }
220
221 #[inline]
222 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
223 (*self).into_view_parts(cx, parts);
224 }
225}
226
227impl<'b, T: ?Sized> AttributeValueViewParts for &&'b T
228where
229 &'b T: AttributeValueViewParts,
230{
231 #[inline]
232 fn attribute_present(&self) -> bool {
233 (**self).attribute_present()
234 }
235
236 #[inline]
237 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
238 (*self).into_view_parts(cx, parts);
239 }
240}
241
242impl<T> AttributeValueViewParts for Option<T>
243where
244 T: AttributeValueViewParts,
245{
246 #[inline]
247 fn attribute_present(&self) -> bool {
248 self.as_ref()
249 .is_some_and(AttributeValueViewParts::attribute_present)
250 }
251
252 #[inline]
253 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
254 if let Some(value) = self {
255 value.into_view_parts(cx, parts);
256 }
257 }
258}
259
260macro_rules! impl_tuple {
261 ($($ty:ident),+) => {
262 impl<$($ty),+> AttributeValueViewParts for ($($ty,)+)
263 where
264 $($ty: AttributeValueViewParts,)+
265 {
266 #[inline]
267 #[allow(non_snake_case)]
268 fn attribute_present(&self) -> bool {
269 let ($($ty,)+) = self;
270 $($ty.attribute_present())||+
271 }
272
273 #[inline]
274 #[allow(non_snake_case)]
275 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
276 let ($($ty,)+) = self;
277 $($ty.into_view_parts(cx, parts);)+
278 }
279 }
280 };
281}
282
283impl_tuple!(T1);
284impl_tuple!(T1, T2);
285impl_tuple!(T1, T2, T3);
286impl_tuple!(T1, T2, T3, T4);
287impl_tuple!(T1, T2, T3, T4, T5);
288impl_tuple!(T1, T2, T3, T4, T5, T6);
289impl_tuple!(T1, T2, T3, T4, T5, T6, T7);
290impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8);
291impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9);
292impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
293impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);
294impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
295
296#[non_exhaustive]
301#[derive(Debug, Default, Clone)]
302pub enum AttributeValue {
303 #[default]
309 Absent,
310 Empty,
313 PromotedStr {
315 value: &'static &'static str,
316 context: HtmlContext,
317 },
318 StaticStr {
320 value: &'static str,
321 context: HtmlContext,
322 },
323 String { value: String, context: HtmlContext },
325}
326
327impl AttributeValue {
328 #[inline]
330 #[must_use]
331 pub fn absent() -> Self {
332 Self::Absent
333 }
334
335 #[inline]
337 #[must_use]
338 pub fn is_present(&self) -> bool {
339 !matches!(self, Self::Absent)
340 }
341
342 #[inline]
345 fn push_into(self, parts: &mut PartsWriter<'_>) {
346 match self {
347 Self::Absent | Self::Empty => {}
348 Self::PromotedStr { value, context } => {
349 parts.in_context(context, |parts| {
350 parts.push_promoted_str(value);
351 });
352 }
353 Self::StaticStr { value, context } => {
354 parts.in_context(context, |parts| {
355 parts.push_static_str(value);
356 });
357 }
358 Self::String { value, context } => {
359 parts.in_context(context, |parts| {
360 parts.push_string(value);
361 });
362 }
363 }
364 }
365
366 #[inline]
369 fn push_ref_into(&self, parts: &mut PartsWriter<'_>) {
370 match self {
371 Self::Absent | Self::Empty => {}
372 Self::PromotedStr { value, context } => {
373 parts.in_context(*context, |parts| {
374 parts.push_promoted_str(value);
375 });
376 }
377 Self::StaticStr { value, context } => {
378 parts.in_context(*context, |parts| {
379 parts.push_static_str(value);
380 });
381 }
382 Self::String { value, context } => {
383 parts.in_context(*context, |parts| {
384 parts.push_str(value);
385 });
386 }
387 }
388 }
389}
390
391impl AttributeValueViewParts for AttributeValue {
392 #[inline]
393 fn attribute_present(&self) -> bool {
394 self.is_present()
395 }
396
397 #[inline]
398 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
399 self.push_into(parts);
400 }
401}
402
403impl AttributeValueViewParts for &AttributeValue {
404 #[inline]
405 fn attribute_present(&self) -> bool {
406 self.is_present()
407 }
408
409 #[inline]
410 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
411 self.push_ref_into(parts);
412 }
413}
414
415impl ClassViewParts for AttributeValue {
419 #[inline]
420 fn is_present(&self) -> bool {
421 AttributeValue::is_present(self)
422 }
423
424 #[inline]
425 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
426 self.push_into(parts);
427 }
428}
429
430impl ClassViewParts for &AttributeValue {
431 #[inline]
432 fn is_present(&self) -> bool {
433 AttributeValue::is_present(self)
434 }
435
436 #[inline]
437 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
438 self.push_ref_into(parts);
439 }
440}
441
442impl Captured for AttributeValue {
443 #[inline]
444 fn empty() -> Self {
445 Self::Empty
446 }
447
448 #[inline]
449 fn promoted_str(value: &'static &'static str, context: HtmlContext) -> Self {
450 Self::PromotedStr { value, context }
451 }
452
453 #[inline]
454 fn static_str(value: &'static str, context: HtmlContext) -> Self {
455 Self::StaticStr { value, context }
456 }
457
458 #[inline]
459 fn string(value: String, context: HtmlContext) -> Self {
460 Self::String { value, context }
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467 use crate::{Attributes, internal::Builder};
468
469 fn capture(value: impl AttributeValueViewParts) -> AttributeValue {
471 let cx = Cx::default();
472 let mut attrs = Attributes::new();
473 attrs.insert(&cx, "x", value);
474 attrs.remove("x").unwrap()
475 }
476
477 fn render(value: &AttributeValue) -> String {
479 let cx = Cx::default();
480 Builder::build(&cx, |b| b.attribute_value(value)).render(&cx)
481 }
482
483 #[test]
484 fn a_single_promoted_string_is_kept_as_is() {
485 let value = capture(PromotedStr(&"a\"b"));
486 assert!(matches!(
487 value,
488 AttributeValue::PromotedStr {
489 value: &"a\"b",
490 context: HtmlContext::AttributeValue,
491 }
492 ));
493 assert_eq!(render(&value), "a"b");
494 }
495
496 #[test]
497 fn a_single_static_string_is_kept_as_is() {
498 let value = capture(StaticStr("a"));
499 assert!(matches!(
500 value,
501 AttributeValue::StaticStr { value: "a", .. }
502 ));
503 assert_eq!(render(&value), "a");
504 }
505
506 #[test]
507 fn a_single_borrowed_string_is_owned_but_not_escaped_yet() {
508 let value = capture("a\"b");
509 let AttributeValue::String { value, context } = &value else {
510 panic!("expected an owned string");
511 };
512 assert_eq!(value, "a\"b");
513 assert_eq!(*context, HtmlContext::AttributeValue);
514 }
515
516 #[test]
517 fn an_unescaped_string_keeps_its_context() {
518 let value = capture(Unescaped::new_unchecked(PromotedStr(&"a"b")));
519 assert!(matches!(
520 value,
521 AttributeValue::PromotedStr {
522 context: HtmlContext::Unescaped,
523 ..
524 }
525 ));
526 assert_eq!(render(&value), "a"b");
527 }
528
529 #[test]
530 fn a_true_boolean_is_empty() {
531 let value = capture(true);
532 assert!(matches!(value, AttributeValue::Empty));
533 assert!(value.is_present());
534 assert_eq!(render(&value), "");
535 }
536
537 #[test]
538 fn a_false_boolean_and_none_are_absent() {
539 assert!(matches!(capture(false), AttributeValue::Absent));
540 assert!(matches!(
541 capture(Option::<&str>::None),
542 AttributeValue::Absent
543 ));
544 }
545
546 #[test]
547 fn multiple_parts_are_rendered_and_escaped_once() {
548 let value = capture(("a\"", 1, Unescaped::new_unchecked(StaticStr("&"))));
549 let AttributeValue::String { value, context } = &value else {
550 panic!("expected a rendered string");
551 };
552 assert_eq!(value, "a"1&");
553 assert_eq!(*context, HtmlContext::Unescaped);
554 }
555
556 #[test]
557 fn a_primitive_is_rendered() {
558 let value = capture(42);
559 assert!(matches!(value, AttributeValue::String { .. }));
560 assert_eq!(render(&value), "42");
561 }
562
563 #[test]
564 fn a_rendered_value_is_not_escaped_again() {
565 let value = capture(("a\"", "b"));
566 assert_eq!(render(&value), "a"b");
567 }
568
569 #[test]
570 fn a_captured_value_captures_as_itself() {
571 let value = capture(PromotedStr(&"a"));
572 let again = capture(&value);
573 assert!(matches!(
574 again,
575 AttributeValue::PromotedStr { value: &"a", .. }
576 ));
577 }
578}