use std::borrow::Cow;
use topcoat_core::context::Cx;
use crate::{AttributeValueViewParts, PartsWriter, PromotedStr, StaticStr, Unescaped};
pub trait ClassViewParts {
fn is_present(&self) -> bool;
fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>);
}
impl ClassViewParts for &str {
#[inline]
fn is_present(&self) -> bool {
!self.is_empty()
}
#[inline]
fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
parts.push_str(self);
}
}
impl ClassViewParts for String {
#[inline]
fn is_present(&self) -> bool {
!self.is_empty()
}
#[inline]
fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
parts.push_string(self);
}
}
impl ClassViewParts for &String {
#[inline]
fn is_present(&self) -> bool {
!self.is_empty()
}
#[inline]
fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
ClassViewParts::into_view_parts(self.as_str(), cx, parts);
}
}
impl ClassViewParts for PromotedStr {
#[inline]
fn is_present(&self) -> bool {
!self.0.is_empty()
}
#[inline]
fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
parts.push_promoted_str(self.0);
}
}
impl ClassViewParts for StaticStr {
#[inline]
fn is_present(&self) -> bool {
!self.0.is_empty()
}
#[inline]
fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
parts.push_static_str(self.0);
}
}
impl ClassViewParts for Unescaped<PromotedStr> {
#[inline]
fn is_present(&self) -> bool {
!self.0.0.is_empty()
}
#[inline]
fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
parts.push_promoted_str_unescaped(self.0.0);
}
}
impl ClassViewParts for Unescaped<StaticStr> {
#[inline]
fn is_present(&self) -> bool {
!self.0.0.is_empty()
}
#[inline]
fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
parts.push_static_str_unescaped(self.0.0);
}
}
impl ClassViewParts for Cow<'static, str> {
#[inline]
fn is_present(&self) -> bool {
!self.is_empty()
}
#[inline]
fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
match self {
Cow::Borrowed(value) => parts.push_static_str(value),
Cow::Owned(value) => parts.push_string(value),
};
}
}
impl<T> ClassViewParts for Option<T>
where
T: ClassViewParts,
{
#[inline]
fn is_present(&self) -> bool {
self.as_ref().is_some_and(T::is_present)
}
#[inline]
fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
if let Some(value) = self {
value.into_view_parts(cx, parts);
}
}
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy)]
pub enum ClassBranch<A, B> {
Then(A),
Else(B),
}
impl<A, B> ClassViewParts for ClassBranch<A, B>
where
A: ClassViewParts,
B: ClassViewParts,
{
#[inline]
fn is_present(&self) -> bool {
match self {
Self::Then(inner) => inner.is_present(),
Self::Else(inner) => inner.is_present(),
}
}
#[inline]
fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
match self {
Self::Then(inner) => inner.into_view_parts(cx, parts),
Self::Else(inner) => inner.into_view_parts(cx, parts),
}
}
}
pub struct ClassWriter<'a, 'b> {
parts: &'b mut PartsWriter<'a>,
first: bool,
}
impl<'a, 'b> ClassWriter<'a, 'b> {
#[inline]
pub(crate) fn new(parts: &'b mut PartsWriter<'a>) -> Self {
Self { parts, first: true }
}
#[inline]
pub fn entry(&mut self, cx: &Cx, value: impl ClassViewParts) -> &mut Self {
if value.is_present() {
if !self.first {
self.parts.push_promoted_str(&" ");
}
value.into_view_parts(cx, self.parts);
self.first = false;
}
self
}
}
pub trait ClassEntries {
fn any_present(&self) -> bool;
fn write_entries(self, cx: &Cx, writer: &mut ClassWriter<'_, '_>);
}
impl<T> ClassEntries for T
where
T: ClassViewParts,
{
#[inline]
fn any_present(&self) -> bool {
self.is_present()
}
#[inline]
fn write_entries(self, cx: &Cx, writer: &mut ClassWriter<'_, '_>) {
writer.entry(cx, self);
}
}
impl ClassEntries for () {
#[inline]
fn any_present(&self) -> bool {
false
}
#[inline]
fn write_entries(self, _cx: &Cx, _writer: &mut ClassWriter<'_, '_>) {}
}
impl<T> ClassEntries for Vec<T>
where
T: ClassEntries,
{
#[inline]
fn any_present(&self) -> bool {
self.iter().any(ClassEntries::any_present)
}
#[inline]
fn write_entries(self, cx: &Cx, writer: &mut ClassWriter<'_, '_>) {
for entry in self {
entry.write_entries(cx, writer);
}
}
}
impl<T, const N: usize> ClassEntries for [T; N]
where
T: ClassEntries,
{
#[inline]
fn any_present(&self) -> bool {
self.iter().any(ClassEntries::any_present)
}
#[inline]
fn write_entries(self, cx: &Cx, writer: &mut ClassWriter<'_, '_>) {
for entry in self {
entry.write_entries(cx, writer);
}
}
}
macro_rules! impl_tuple {
($($ty:ident),+) => {
impl<$($ty),+> ClassEntries for ($($ty,)+)
where
$($ty: ClassEntries,)+
{
#[inline]
#[allow(non_snake_case)]
fn any_present(&self) -> bool {
let ($($ty,)+) = self;
$($ty.any_present())||+
}
#[inline]
#[allow(non_snake_case)]
fn write_entries(self, cx: &Cx, writer: &mut ClassWriter<'_, '_>) {
let ($($ty,)+) = self;
$($ty.write_entries(cx, writer);)+
}
}
};
}
impl_tuple!(T1);
impl_tuple!(T1, T2);
impl_tuple!(T1, T2, T3);
impl_tuple!(T1, T2, T3, T4);
impl_tuple!(T1, T2, T3, T4, T5);
impl_tuple!(T1, T2, T3, T4, T5, T6);
impl_tuple!(T1, T2, T3, T4, T5, T6, T7);
impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8);
impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9);
impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);
impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
#[derive(Debug, Default, Clone, Copy)]
pub struct Class<T>(pub T);
impl<T> AttributeValueViewParts for Class<T>
where
T: ClassEntries,
{
#[inline]
fn attribute_present(&self) -> bool {
self.0.any_present()
}
#[inline]
fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
self.0.write_entries(cx, &mut ClassWriter::new(parts));
}
}
impl<T> ClassViewParts for Class<T>
where
T: ClassEntries,
{
#[inline]
fn is_present(&self) -> bool {
self.0.any_present()
}
#[inline]
fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
self.0.write_entries(cx, &mut ClassWriter::new(parts));
}
}
pub type StaticClass = Class<Unescaped<PromotedStr>>;
#[cfg(test)]
mod tests {
use super::*;
use crate::{AttributeValue, Attributes, internal::Builder};
fn render(class: Class<impl ClassEntries>) -> String {
let cx = Cx::default();
Builder::build(&cx, |b| b.attribute_value(class)).render(&cx)
}
#[test]
fn no_entries_is_absent() {
let class = Class(());
assert!(!class.attribute_present());
assert_eq!(render(class), "");
}
#[test]
fn single_entry_renders_without_separator() {
let class = Class("btn");
assert!(class.attribute_present());
assert_eq!(render(class), "btn");
}
#[test]
fn entries_are_separated_by_single_spaces() {
let class = Class(("btn", "btn-lg".to_owned(), Cow::Borrowed("active")));
assert_eq!(render(class), "btn btn-lg active");
}
#[test]
fn none_option_is_skipped_without_a_separator() {
let class = Class(("a", Option::<&str>::None, "b"));
assert_eq!(render(class), "a b");
}
#[test]
fn some_option_is_rendered() {
assert_eq!(render(Class(Some("active"))), "active");
}
#[test]
fn empty_strings_are_skipped_without_a_separator() {
let class = Class(("a", "", String::new(), Some(""), "b"));
assert_eq!(render(class), "a b");
}
#[test]
fn all_entries_absent_omits_the_attribute() {
let class = Class(("", Option::<&str>::None));
assert!(!class.attribute_present());
assert_eq!(render(class), "");
}
#[test]
fn skipped_leading_entry_produces_no_leading_space() {
let class = Class((Option::<&str>::None, "a"));
assert_eq!(render(class), "a");
}
#[test]
fn entries_are_escaped_for_the_attribute_value_position() {
assert_eq!(render(Class("a\"b")), "a"b");
}
#[test]
fn branch_entries_render_the_taken_branch() {
let class = Class((
ClassBranch::<&str, String>::Then("on"),
ClassBranch::<&str, String>::Else("off".to_owned()),
));
assert_eq!(render(class), "on off");
}
#[test]
fn nested_class_is_spliced_with_separators() {
let class = Class(("card", Class(("btn", "btn-lg"))));
assert_eq!(render(class), "card btn btn-lg");
}
#[test]
fn empty_nested_class_is_skipped_without_a_separator() {
let class = Class(("a", Class(()), "b"));
assert_eq!(render(class), "a b");
}
#[test]
fn nested_tuples_flatten_with_separators() {
let class = Class((("a", "b"), ("c", "d")));
assert_eq!(render(class), "a b c d");
}
#[test]
fn vec_entries_render_with_separators() {
let class = Class(vec!["a".to_owned(), String::new(), "b".to_owned()]);
assert_eq!(render(class), "a b");
}
#[test]
fn array_entries_render_with_separators() {
let class = Class([Some("a"), None, Some("b")]);
assert_eq!(render(class), "a b");
}
#[test]
fn attribute_value_entries_are_spliced_as_captured() {
let cx = Cx::default();
let mut attrs = Attributes::new();
attrs.insert(&cx, "class", "[&>*]:mt-2");
let value = attrs.remove("class").unwrap();
assert_eq!(render(Class(("btn", &value))), "btn [&>*]:mt-2");
}
#[test]
fn absent_attribute_value_entry_is_skipped_without_a_separator() {
let value = AttributeValue::absent();
assert_eq!(render(Class(("a", &value, "b"))), "a b");
}
#[test]
fn promoted_and_static_entries_render_with_separators() {
let class = Class((PromotedStr(&"btn"), StaticStr("btn-lg")));
assert_eq!(render(class), "btn btn-lg");
}
#[test]
fn empty_promoted_and_static_entries_are_skipped_without_a_separator() {
let class = Class((StaticStr("a"), PromotedStr(&""), StaticStr(""), "b"));
assert!(class.attribute_present());
assert_eq!(render(class), "a b");
}
#[test]
fn promoted_and_static_entries_are_escaped_for_the_attribute_value_position() {
let class = Class((PromotedStr(&"a\"b"), StaticStr("c\"d")));
assert_eq!(render(class), "a"b c"d");
}
#[test]
fn unescaped_entries_render_verbatim() {
let class = Class((
Unescaped::new_unchecked(PromotedStr(&"a"b")),
Unescaped::new_unchecked(StaticStr("c&d")),
));
assert_eq!(render(class), "a"b c&d");
}
#[test]
fn empty_unescaped_entries_are_skipped_without_a_separator() {
let class = Class((
"a",
Unescaped::new_unchecked(PromotedStr(&"")),
Unescaped::new_unchecked(StaticStr("")),
"b",
));
assert_eq!(render(class), "a b");
}
#[test]
fn borrowed_cow_entries_render_without_reallocating() {
let class = Class((
Cow::Borrowed("static"),
Cow::<'static, str>::Owned("owned".to_owned()),
));
assert_eq!(render(class), "static owned");
}
}