leptos_classes/
class_name.rs1use std::borrow::Cow;
2use std::ops::Deref;
3
4#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
6pub enum InvalidClassName {
7 #[error("Class name is empty or whitespace-only: '{name}'")]
11 Empty {
12 name: Cow<'static, str>,
14 },
15
16 #[error("Class names must not be whitespace-separated. Got: '{name}'.")]
21 ContainsWhitespace {
22 name: Cow<'static, str>,
24 },
25}
26
27#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct ClassName(Cow<'static, str>);
35
36impl ClassName {
37 pub fn try_new(name: impl Into<Cow<'static, str>>) -> Result<Self, InvalidClassName> {
44 let name = name.into();
45 if name.trim().is_empty() {
46 return Err(InvalidClassName::Empty { name });
47 }
48 if name.chars().any(char::is_whitespace) {
49 return Err(InvalidClassName::ContainsWhitespace { name });
50 }
51 Ok(Self(name))
52 }
53
54 #[must_use]
56 pub fn as_str(&self) -> &str {
57 &self.0
58 }
59}
60
61impl Deref for ClassName {
62 type Target = str;
63
64 fn deref(&self) -> &str {
65 self.as_str()
66 }
67}
68
69impl AsRef<str> for ClassName {
70 fn as_ref(&self) -> &str {
71 self.as_str()
72 }
73}
74
75impl From<&'static str> for ClassName {
76 fn from(s: &'static str) -> Self {
77 Self::try_new(s).unwrap_or_else(|err| panic!("{err}"))
78 }
79}
80
81impl From<String> for ClassName {
82 fn from(s: String) -> Self {
83 Self::try_new(s).unwrap_or_else(|err| panic!("{err}"))
84 }
85}
86
87impl From<Cow<'static, str>> for ClassName {
88 fn from(s: Cow<'static, str>) -> Self {
89 Self::try_new(s).unwrap_or_else(|err| panic!("{err}"))
90 }
91}
92
93#[cfg(test)]
94mod try_new {
95 use assertr::prelude::*;
96
97 use super::{ClassName, InvalidClassName};
98
99 #[test]
100 fn accepts_plain_ascii_token() {
101 let name = ClassName::try_new("btn-primary").unwrap();
102 assert_that!(name.as_str()).is_equal_to("btn-primary");
103 }
104
105 #[test]
106 fn accepts_non_whitespace_unicode_token() {
107 let name = ClassName::try_new("h\u{00E9}ros").unwrap();
109 assert_that!(name.as_str()).is_equal_to("h\u{00E9}ros");
110 }
111
112 #[test]
113 fn rejects_empty_input() {
114 assert_that!(ClassName::try_new("")).is_err();
115 }
116
117 #[test]
118 fn rejects_ascii_whitespace_only_input() {
119 let result = ClassName::try_new(" \t\n");
120 assert_that!(result)
121 .is_err()
122 .is_equal_to(InvalidClassName::Empty {
123 name: " \t\n".into(),
124 });
125 }
126
127 #[test]
128 fn rejects_unicode_whitespace_only_input() {
129 let result = ClassName::try_new("\u{00A0}\u{00A0}");
131 assert_that!(result)
132 .is_err()
133 .is_equal_to(InvalidClassName::Empty {
134 name: "\u{00A0}\u{00A0}".into(),
135 });
136 }
137
138 #[test]
139 fn rejects_token_with_ascii_whitespace_in_middle() {
140 let result = ClassName::try_new("foo bar");
141 assert_that!(result)
142 .is_err()
143 .is_equal_to(InvalidClassName::ContainsWhitespace {
144 name: "foo bar".into(),
145 });
146 }
147
148 #[test]
149 fn rejects_token_with_non_breaking_space_in_middle() {
150 let result = ClassName::try_new("foo\u{00A0}bar");
153 assert_that!(result)
154 .is_err()
155 .is_equal_to(InvalidClassName::ContainsWhitespace {
156 name: "foo\u{00A0}bar".into(),
157 });
158 }
159
160 #[test]
161 fn rejects_token_with_line_separator_in_middle() {
162 let result = ClassName::try_new("foo\u{2028}bar");
164 assert_that!(result)
165 .is_err()
166 .is_equal_to(InvalidClassName::ContainsWhitespace {
167 name: "foo\u{2028}bar".into(),
168 });
169 }
170}