1#![forbid(unsafe_code)]
26
27extern crate self as pliego_css;
28
29use core::fmt;
30
31#[doc(hidden)]
33pub mod __private {
34 pub use pliego_css_macros::{pc_id, pcx_id};
35
36 #[must_use]
38 pub const fn style_from_compiled_id(id: u128) -> super::Style {
39 super::Style {
40 id: super::StyleId(pliego_css_ir::StyleId::new(id)),
41 }
42 }
43}
44
45#[macro_export]
50macro_rules! pc {
51 ($($tokens:tt)*) => {
52 $crate::__private::style_from_compiled_id($crate::__private::pc_id!($($tokens)*))
53 };
54}
55
56#[macro_export]
60macro_rules! pcx {
61 ($($tokens:tt)*) => {
62 $crate::__private::style_from_compiled_id($crate::__private::pcx_id!($($tokens)*))
63 };
64}
65
66#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
71#[repr(transparent)]
72pub struct StyleId(pliego_css_ir::StyleId);
73
74impl StyleId {
75 #[must_use]
77 pub const fn get(self) -> u128 {
78 self.0.get()
79 }
80
81 #[must_use]
83 pub const fn is_unresolved(self) -> bool {
84 self.0.is_unresolved()
85 }
86
87 #[must_use]
93 pub fn to_class_name(self) -> String {
94 self.0.to_class_name()
95 }
96}
97
98#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
107#[repr(transparent)]
108pub struct Style {
109 id: StyleId,
110}
111
112impl Style {
113 pub const EMPTY: Self = Self {
115 id: StyleId(pliego_css_ir::StyleId::UNRESOLVED),
116 };
117
118 #[must_use]
120 pub const fn id(self) -> StyleId {
121 self.id
122 }
123
124 #[must_use]
128 pub fn class_name(self) -> String {
129 if self.is_empty() {
130 String::new()
131 } else {
132 self.id.to_class_name()
133 }
134 }
135
136 #[must_use]
138 pub const fn is_empty(self) -> bool {
139 self.id.is_unresolved()
140 }
141}
142
143impl fmt::Display for Style {
144 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
145 if self.is_empty() {
146 Ok(())
147 } else {
148 formatter.write_str(&self.id.to_class_name())
149 }
150 }
151}
152
153impl From<Style> for String {
154 fn from(style: Style) -> Self {
155 style.class_name()
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 const REUSABLE_CARD: Style = pc!("rounded-lg bg-surface p-6");
164
165 #[test]
166 fn macro_returns_validated_static_style() {
167 let style = pc!("flex items-center gap-4");
168 assert!(!style.id().is_unresolved());
169 assert!(style.class_name().starts_with("pc_"));
170 assert!(!style.is_empty());
171 }
172
173 #[test]
174 fn empty_style_is_explicit() {
175 assert!(Style::EMPTY.is_empty());
176 assert!(Style::EMPTY.id().is_unresolved());
177 assert_eq!(Style::EMPTY.id().to_class_name(), "pc_0");
178 assert!(Style::EMPTY.class_name().is_empty());
179 assert_eq!(Style::EMPTY.to_string(), "");
180 }
181
182 #[test]
183 fn style_formats_as_its_css_class() {
184 let style = pc!("flex gap-4");
185
186 assert_eq!(style.to_string(), style.class_name());
187 assert_eq!(String::from(style), style.class_name());
188 }
189
190 #[test]
191 fn runtime_handle_is_only_the_style_identity() {
192 assert_eq!(
193 core::mem::size_of::<StyleId>(),
194 core::mem::size_of::<u128>()
195 );
196 assert_eq!(
197 core::mem::size_of::<Style>(),
198 core::mem::size_of::<StyleId>()
199 );
200 }
201
202 #[test]
203 fn validated_styles_are_reusable_constants() {
204 assert_eq!(REUSABLE_CARD.id(), pc!("p-6 bg-surface rounded-lg").id());
205 }
206
207 #[test]
208 fn equivalent_utility_order_has_one_identity() {
209 assert_eq!(pc!("flex gap-4").id(), pc!("gap-4 flex").id());
210 }
211
212 #[test]
213 fn conditional_if_returns_a_complete_precompiled_style() {
214 let selected = true;
215 let style = pcx!(
216 "inline-flex rounded-md bg-surface",
217 if selected {
218 "bg-accent text-white"
219 } else {
220 "text-ink"
221 },
222 );
223
224 assert_eq!(
225 style.id(),
226 pc!("inline-flex rounded-md bg-accent text-white").id()
227 );
228 }
229
230 #[test]
231 fn conditional_match_compiles_every_literal_arm() {
232 enum Variant {
233 Primary,
234 Ghost,
235 }
236
237 let variant = Variant::Ghost;
238 let style = pcx!(
239 "rounded-md",
240 match variant {
241 Variant::Primary => "bg-accent text-white",
242 Variant::Ghost => "bg-transparent text-accent",
243 },
244 );
245
246 assert_eq!(
247 style.id(),
248 pc!("rounded-md bg-transparent text-accent").id()
249 );
250 let _ = Variant::Primary;
251 }
252
253 #[test]
254 fn independent_clauses_select_one_precompiled_cartesian_result() {
255 let active = false;
256 let compact = true;
257 let style = pcx!(
258 "inline-flex rounded-md",
259 if active { "opacity-100" } else { "opacity-50" },
260 if compact { "text-sm" } else { "text-lg" },
261 );
262
263 assert_eq!(
264 style.id(),
265 pc!("inline-flex rounded-md opacity-50 text-sm").id()
266 );
267 }
268
269 #[test]
270 fn each_independent_clause_expression_is_evaluated_once() {
271 use core::cell::Cell;
272
273 let evaluations = Cell::new(0_u8);
274 let condition = || {
275 evaluations.set(evaluations.get() + 1);
276 true
277 };
278 let style = pcx!(
279 "block",
280 if condition() {
281 "opacity-100"
282 } else {
283 "opacity-50"
284 },
285 if condition() { "text-sm" } else { "text-lg" },
286 );
287
288 assert_eq!(evaluations.get(), 2);
289 assert_eq!(style.id(), pc!("block opacity-100 text-sm").id());
290 }
291
292 #[test]
293 fn independent_if_and_match_preserve_rust_selection_semantics() {
294 enum Size {
295 Small,
296 Large,
297 }
298
299 let active = true;
300 let size = Size::Large;
301 let style = pcx!(
302 "inline-flex",
303 if active { "opacity-100" } else { "opacity-50" },
304 match size {
305 Size::Small => "text-sm",
306 Size::Large => "text-lg",
307 },
308 );
309
310 assert_eq!(style.id(), pc!("inline-flex opacity-100 text-lg").id());
311 let _ = Size::Small;
312 }
313}