stylance/
lib.rs

1//! # About stylance
2//!
3//! Stylance is a scoped CSS library for rust.
4//!
5//! Use it in conjunction with [stylance-cli](https://crates.io/crates/stylance-cli).
6//!
7//! # Usage
8//!
9//! Create a .module.css file inside your rust source directory
10//! ```scss
11//! // src/component1/style.module.css
12//!
13//! .header {
14//!     color: red;
15//! }
16//!
17//! .contents {
18//!     border: 1px solid black;
19//! }
20//! ```
21//!
22//! Then import that file from your rust code:
23//! ```rust
24//! stylance::import_crate_style!(style, "src/component1/style.module.css");
25//! stylance::import_style!(style2, "style2.module.css");
26//!
27//! fn use_style() {
28//!     println!("{} {}", style::header, style2::content);
29//! }
30//! ```
31//!
32//! ### Accessing non-scoped global class names with `:global(.class)`
33//!
34//! Sometimes you may want to use an external classname in your .module.css file.
35//!
36//! For this you can wrap the global class name with `:global()`, this instructs stylance to leave that class name alone.
37//!
38//! ```css
39//! .contents :global(.paragraph) {
40//!     color: blue;
41//! }
42//! ```
43//!
44//! This will expand to
45//! ```css
46//! .contents-539306b .paragraph {
47//!     color: blue;
48//! }
49//! ```
50//!
51//! # Transforming and bundling your .module.css files
52//!
53//! To transform your .module.css and .module.scss into a bundled css file use [stylance-cli](https://crates.io/crates/stylance-cli).
54//!
55//!
56
57#![cfg_attr(docsrs, feature(doc_cfg))]
58
59#[doc(hidden)]
60pub mod internal {
61    /// MaybeStr Wraps an Option<&str> and implements From trait for various
62    /// types.
63    /// Used by JoinClasses and the classes! macro to accept various types.
64    pub struct MaybeStr<'a>(Option<&'a str>);
65
66    pub use stylance_macros::*;
67
68    fn join_opt_str_iter<'a, Iter>(iter: &mut Iter) -> String
69    where
70        Iter: Iterator<Item = &'a str> + Clone,
71    {
72        let Some(first) = iter.next() else {
73            return String::new();
74        };
75
76        let size = first.len() + iter.clone().map(|v| v.len() + 1).sum::<usize>();
77
78        let mut result = String::with_capacity(size);
79        result.push_str(first);
80
81        for v in iter {
82            result.push(' ');
83            result.push_str(v);
84        }
85
86        debug_assert_eq!(result.len(), size);
87
88        result
89    }
90
91    pub fn join_maybe_str_slice(slice: &[MaybeStr<'_>]) -> String {
92        let mut iter = slice.iter().flat_map(|c| c.0);
93        join_opt_str_iter(&mut iter)
94    }
95
96    impl<'a> From<&'a str> for MaybeStr<'a> {
97        fn from(value: &'a str) -> Self {
98            MaybeStr::<'a>(Some(value))
99        }
100    }
101
102    impl<'a> From<&'a String> for MaybeStr<'a> {
103        fn from(value: &'a String) -> Self {
104            MaybeStr::<'a>(Some(value.as_ref()))
105        }
106    }
107
108    impl<'a, T> From<Option<&'a T>> for MaybeStr<'a>
109    where
110        T: AsRef<str> + ?Sized,
111    {
112        fn from(value: Option<&'a T>) -> Self {
113            Self(value.map(AsRef::as_ref))
114        }
115    }
116
117    impl<'a, T> From<&'a Option<T>> for MaybeStr<'a>
118    where
119        T: AsRef<str>,
120    {
121        fn from(value: &'a Option<T>) -> Self {
122            Self(value.as_ref().map(AsRef::as_ref))
123        }
124    }
125}
126
127/// Reads a css file at compile time and generates a module containing the classnames found inside that css file.
128/// Path is relative to the file that called the macro.
129///
130/// ### Syntax
131/// ```rust
132/// import_style!([#[attribute]] [pub] module_identifier, style_path);
133/// ```
134/// - Optionally prefix attributes that will be added to the generated module. Particularly common is `#[allow(dead_code)]` to silence warnings from unused class names.
135/// - Optionally add pub keyword before `module_identifier` to make the generated module public.
136/// - `module_identifier`: This will be used as the name of the module generated by this macro.
137/// - `style_path`: This should be a string literal with the path to a css file inside your rust
138///   crate. The path is relative to the file where this macro was called from.
139///
140/// ### Example
141/// ```rust
142/// // style.css is located in the same directory as this rust file.
143/// stylance::import_style!(#[allow(dead_code)] pub style, "style.css");
144///
145/// fn use_style() {
146///     println!("{}", style::header);
147/// }
148/// ```
149///
150/// ### Expands into
151///
152/// ```rust
153/// pub mod style {
154///     pub const header: &str = "header-539306b";
155///     pub const contents: &str = "contents-539306b";
156/// }
157/// ```
158#[macro_export]
159macro_rules! import_style {
160    ($(#[$meta:meta])* $vis:vis $ident:ident, $str:expr) => {
161        $(#[$meta])* $vis mod $ident {
162            ::stylance::internal::import_style_classes_rel!($str);
163        }
164    };
165}
166
167/// Reads a css file at compile time and generates a module containing the classnames found inside that css file.
168///
169/// ### Syntax
170/// ```rust
171/// import_crate_style!([#[attribute]] [pub] module_identifier, style_path);
172/// ```
173/// - Optionally prefix attributes that will be added to the generated module. Particularly common is `#[allow(dead_code)]` to silence warnings from unused class names.
174/// - Optionally add pub keyword before `module_identifier` to make the generated module public.
175/// - `module_identifier`: This will be used as the name of the module generated by this macro.
176/// - `style_path`: This should be a string literal with the path to a css file inside your rust
177///   crate. The path must be relative to the cargo manifest directory (The directory that has Cargo.toml).
178///
179/// ### Example
180/// ```rust
181/// stylance::import_crate_style!(pub style, "path/from/manifest_dir/to/style.css");
182///
183/// fn use_style() {
184///     println!("{}", style::header);
185/// }
186/// ```
187///
188/// ### Expands into
189///
190/// ```rust
191/// pub mod style {
192///     pub const header: &str = "header-539306b";
193///     pub const contents: &str = "contents-539306b";
194/// }
195/// ```
196#[macro_export]
197macro_rules! import_crate_style {
198    ($(#[$meta:meta])* $vis:vis $ident:ident, $str:expr) => {
199        $(#[$meta])* $vis mod $ident {
200            ::stylance::internal::import_style_classes!($str);
201        }
202    };
203}
204
205/// Utility trait for combining tuples of class names into a single string.
206pub trait JoinClasses {
207    /// Join all elements of the tuple into a single string separating them with a single space character.
208    ///
209    /// Option elements of the tuple will be skipped if they are None.
210    ///
211    /// ### Example
212    ///
213    /// ```rust
214    /// import_crate_style!(style, "tests/style.module.scss");
215    /// let current_page = 10; // Some variable to use in the condition
216    ///
217    /// let class_name = (
218    ///     "header",      // Global classname
219    ///     style::style1, // Stylance scoped classname
220    ///     if current_page == 10 { // Conditional class
221    ///         Some("active1")
222    ///     } else {
223    ///         None
224    ///     },
225    ///     (current_page == 11).then_some("active2"), // Same as above but much nicer
226    /// )
227    ///     .join_classes();
228    ///
229    /// // class_name is "header style1-a331da9 active1"
230    /// ```
231    fn join_classes(self) -> String;
232}
233
234impl JoinClasses for &[internal::MaybeStr<'_>] {
235    fn join_classes(self) -> String {
236        internal::join_maybe_str_slice(self)
237    }
238}
239
240macro_rules! impl_join_classes_for_tuples {
241    (($($types:ident),*), ($($idx:tt),*)) => {
242            impl<'a, $($types),*> JoinClasses for ($($types,)*)
243            where
244                $($types: Into<internal::MaybeStr<'a>>),*
245            {
246                fn join_classes(self) -> String {
247                    internal::join_maybe_str_slice([
248                        $((self.$idx).into()),*
249                    ].as_slice())
250                }
251            }
252    };
253}
254
255impl_join_classes_for_tuples!(
256    (T1, T2), //
257    (0, 1)
258);
259impl_join_classes_for_tuples!(
260    (T1, T2, T3), //
261    (0, 1, 2)
262);
263impl_join_classes_for_tuples!(
264    (T1, T2, T3, T4), //
265    (0, 1, 2, 3)
266);
267impl_join_classes_for_tuples!(
268    (T1, T2, T3, T4, T5), //
269    (0, 1, 2, 3, 4)
270);
271impl_join_classes_for_tuples!(
272    (T1, T2, T3, T4, T5, T6), //
273    (0, 1, 2, 3, 4, 5)
274);
275impl_join_classes_for_tuples!(
276    (T1, T2, T3, T4, T5, T6, T7), //
277    (0, 1, 2, 3, 4, 5, 6)
278);
279impl_join_classes_for_tuples!(
280    (T1, T2, T3, T4, T5, T6, T7, T8), //
281    (0, 1, 2, 3, 4, 5, 6, 7)
282);
283impl_join_classes_for_tuples!(
284    (T1, T2, T3, T4, T5, T6, T7, T8, T9),
285    (0, 1, 2, 3, 4, 5, 6, 7, 8)
286);
287impl_join_classes_for_tuples!(
288    (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10),
289    (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
290);
291impl_join_classes_for_tuples!(
292    (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11),
293    (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
294);
295impl_join_classes_for_tuples!(
296    (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12),
297    (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
298);
299impl_join_classes_for_tuples!(
300    (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13),
301    (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
302);
303impl_join_classes_for_tuples!(
304    (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14),
305    (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)
306);
307impl_join_classes_for_tuples!(
308    (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15),
309    (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)
310);
311impl_join_classes_for_tuples!(
312    (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16),
313    (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
314);
315impl_join_classes_for_tuples!(
316    (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17),
317    (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
318);
319
320/// Utility macro for joining multiple class names.
321///
322/// The macro accepts `&str` `&String` and any refs of `T` where `T` implements `AsRef<str>`
323///
324/// It also accepts `Option` of those types, `None` values will be filtered from the list.
325///
326/// Example
327///
328/// ```rust
329/// let active_tab = 0; // set to 1 to disable the active class!
330/// let classes_string = classes!(
331///     "some-global-class",
332///     my_style::header,
333///     module_style::header,
334///     // conditionally activate a global style
335///     if active_tab == 0 { Some(my_style::active) } else { None }
336///     // The same can be expressed with then_some:
337///     (active_tab == 0).then_some(my_style::active)
338/// );
339/// ```
340#[macro_export]
341macro_rules! classes {
342    () => { "" };
343    ($($exp:expr),+$(,)?) => {
344        ::stylance::JoinClasses::join_classes([$($exp.into()),*].as_slice())
345    };
346}