stylance 0.6.0

Scoped CSS for rust projects
Documentation
//! # About stylance
//!
//! Stylance is a scoped CSS library for rust.
//!
//! Use it in conjunction with [stylance-cli](https://crates.io/crates/stylance-cli).
//!
//! # Feature flags
//!
//! - `nightly`: Enables importing styles with paths relative to the rust file where the macro was called.
//!
//! # Usage
//!
//! Create a .module.css file inside your rust source directory
//! ```scss
//! // src/component1/style.module.css
//!
//! .header {
//!     color: red;
//! }
//!
//! .contents {
//!     border: 1px solid black;
//! }
//! ```
//!
//! Then import that file from your rust code:
//! ```rust
//! stylance::import_crate_style!(style, "src/component1/style.module.css");
//!
//! fn use_style() {
//!     println!("{}", style::header);
//! }
//! ```
//!
//! ### Accessing non-scoped global class names with `:global(.class)`
//!
//! Sometimes you may want to use an external classname in your .module.css file.
//!
//! For this you can wrap the global class name with `:global()`, this instructs stylance to leave that class name alone.
//!
//! ```css
//! .contents :global(.paragraph) {
//!     color: blue;
//! }
//! ```
//!
//! This will expand to
//! ```css
//! .contents-539306b .paragraph {
//!     color: blue;
//! }
//! ```
//!
//! # Transforming and bundling your .module.css files
//!
//! To transform your .module.css and .module.scss into a bundled css file use [stylance-cli](https://crates.io/crates/stylance-cli).
//!
//!

#![cfg_attr(docsrs, feature(doc_cfg))]

#[doc(hidden)]
pub mod internal {
    /// MaybeStr Wraps an Option<&str> and implements From trait for various
    /// types.
    /// Used by JoinClasses and the classes! macro to accept various types.
    pub struct MaybeStr<'a>(Option<&'a str>);

    pub use stylance_macros::*;

    fn join_opt_str_iter<'a, Iter>(iter: &mut Iter) -> String
    where
        Iter: Iterator<Item = &'a str> + Clone,
    {
        let Some(first) = iter.next() else {
            return String::new();
        };

        let size = first.len() + iter.clone().map(|v| v.len() + 1).sum::<usize>();

        let mut result = String::with_capacity(size);
        result.push_str(first);

        for v in iter {
            result.push(' ');
            result.push_str(v);
        }

        debug_assert_eq!(result.len(), size);

        result
    }

    pub fn join_maybe_str_slice(slice: &[MaybeStr<'_>]) -> String {
        let mut iter = slice.iter().flat_map(|c| c.0);
        join_opt_str_iter(&mut iter)
    }

    impl<'a> From<&'a str> for MaybeStr<'a> {
        fn from(value: &'a str) -> Self {
            MaybeStr::<'a>(Some(value))
        }
    }

    impl<'a> From<&'a String> for MaybeStr<'a> {
        fn from(value: &'a String) -> Self {
            MaybeStr::<'a>(Some(value.as_ref()))
        }
    }

    impl<'a, T> From<Option<&'a T>> for MaybeStr<'a>
    where
        T: AsRef<str> + ?Sized,
    {
        fn from(value: Option<&'a T>) -> Self {
            Self(value.map(AsRef::as_ref))
        }
    }

    impl<'a, T> From<&'a Option<T>> for MaybeStr<'a>
    where
        T: AsRef<str>,
    {
        fn from(value: &'a Option<T>) -> Self {
            Self(value.as_ref().map(AsRef::as_ref))
        }
    }
}

/// Reads a css file at compile time and generates a module containing the classnames found inside that css file.
/// Path is relative to the file that called the macro.
///
/// ### Syntax
/// ```rust
/// import_style!([#[attribute]] [pub] module_identifier, style_path);
/// ```
/// - Optionally prefix attributes that will be added to the generated module. Particularly common is `#[allow(dead_code)]` to silence warnings from unused class names.
/// - Optionally add pub keyword before `module_identifier` to make the generated module public.
/// - `module_identifier`: This will be used as the name of the module generated by this macro.
/// - `style_path`: This should be a string literal with the path to a css file inside your rust
///   crate. The path is relative to the file where this macro was called from.
///
/// ### Example
/// ```rust
/// // style.css is located in the same directory as this rust file.
/// stylance::import_style!(#[allow(dead_code)] pub style, "style.css");
///
/// fn use_style() {
///     println!("{}", style::header);
/// }
/// ```
///
/// ### Expands into
///
/// ```rust
/// pub mod style {
///     pub const header: &str = "header-539306b";
///     pub const contents: &str = "contents-539306b";
/// }
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "nightly")))]
#[macro_export]
macro_rules! import_style {
    ($(#[$meta:meta])* $vis:vis $ident:ident, $str:expr) => {
        $(#[$meta])* $vis mod $ident {
            ::stylance::internal::import_style_classes_rel!($str);
        }
    };
}

/// Reads a css file at compile time and generates a module containing the classnames found inside that css file.
///
/// ### Syntax
/// ```rust
/// import_crate_style!([#[attribute]] [pub] module_identifier, style_path);
/// ```
/// - Optionally prefix attributes that will be added to the generated module. Particularly common is `#[allow(dead_code)]` to silence warnings from unused class names.
/// - Optionally add pub keyword before `module_identifier` to make the generated module public.
/// - `module_identifier`: This will be used as the name of the module generated by this macro.
/// - `style_path`: This should be a string literal with the path to a css file inside your rust
///   crate. The path must be relative to the cargo manifest directory (The directory that has Cargo.toml).
///
/// ### Example
/// ```rust
/// stylance::import_crate_style!(pub style, "path/from/manifest_dir/to/style.css");
///
/// fn use_style() {
///     println!("{}", style::header);
/// }
/// ```
///
/// ### Expands into
///
/// ```rust
/// pub mod style {
///     pub const header: &str = "header-539306b";
///     pub const contents: &str = "contents-539306b";
/// }
/// ```
#[macro_export]
macro_rules! import_crate_style {
    ($(#[$meta:meta])* $vis:vis $ident:ident, $str:expr) => {
        $(#[$meta])* $vis mod $ident {
            ::stylance::internal::import_style_classes!($str);
        }
    };
}

/// Utility trait for combining tuples of class names into a single string.
pub trait JoinClasses {
    /// Join all elements of the tuple into a single string separating them with a single space character.
    ///
    /// Option elements of the tuple will be skipped if they are None.
    ///
    /// ### Example
    ///
    /// ```rust
    /// import_crate_style!(style, "tests/style.module.scss");
    /// let current_page = 10; // Some variable to use in the condition
    ///
    /// let class_name = (
    ///     "header",      // Global classname
    ///     style::style1, // Stylance scoped classname
    ///     if current_page == 10 { // Conditional class
    ///         Some("active1")
    ///     } else {
    ///         None
    ///     },
    ///     (current_page == 11).then_some("active2"), // Same as above but much nicer
    /// )
    ///     .join_classes();
    ///
    /// // class_name is "header style1-a331da9 active1"
    /// ```
    fn join_classes(self) -> String;
}

impl JoinClasses for &[internal::MaybeStr<'_>] {
    fn join_classes(self) -> String {
        internal::join_maybe_str_slice(self)
    }
}

macro_rules! impl_join_classes_for_tuples {
    (($($types:ident),*), ($($idx:tt),*)) => {
            impl<'a, $($types),*> JoinClasses for ($($types,)*)
            where
                $($types: Into<internal::MaybeStr<'a>>),*
            {
                fn join_classes(self) -> String {
                    internal::join_maybe_str_slice([
                        $((self.$idx).into()),*
                    ].as_slice())
                }
            }
    };
}

impl_join_classes_for_tuples!(
    (T1, T2), //
    (0, 1)
);
impl_join_classes_for_tuples!(
    (T1, T2, T3), //
    (0, 1, 2)
);
impl_join_classes_for_tuples!(
    (T1, T2, T3, T4), //
    (0, 1, 2, 3)
);
impl_join_classes_for_tuples!(
    (T1, T2, T3, T4, T5), //
    (0, 1, 2, 3, 4)
);
impl_join_classes_for_tuples!(
    (T1, T2, T3, T4, T5, T6), //
    (0, 1, 2, 3, 4, 5)
);
impl_join_classes_for_tuples!(
    (T1, T2, T3, T4, T5, T6, T7), //
    (0, 1, 2, 3, 4, 5, 6)
);
impl_join_classes_for_tuples!(
    (T1, T2, T3, T4, T5, T6, T7, T8), //
    (0, 1, 2, 3, 4, 5, 6, 7)
);
impl_join_classes_for_tuples!(
    (T1, T2, T3, T4, T5, T6, T7, T8, T9),
    (0, 1, 2, 3, 4, 5, 6, 7, 8)
);
impl_join_classes_for_tuples!(
    (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10),
    (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
);
impl_join_classes_for_tuples!(
    (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11),
    (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
);
impl_join_classes_for_tuples!(
    (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12),
    (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
);
impl_join_classes_for_tuples!(
    (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13),
    (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
);
impl_join_classes_for_tuples!(
    (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14),
    (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)
);
impl_join_classes_for_tuples!(
    (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15),
    (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)
);
impl_join_classes_for_tuples!(
    (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16),
    (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
);
impl_join_classes_for_tuples!(
    (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17),
    (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
);

/// Utility macro for joining multiple class names.
///
/// The macro accepts `&str` `&String` and any refs of `T` where `T` implements `AsRef<str>`
///
/// It also accepts `Option` of those types, `None` values will be filtered from the list.
///
/// Example
///
/// ```rust
/// let active_tab = 0; // set to 1 to disable the active class!
/// let classes_string = classes!(
///     "some-global-class",
///     my_style::header,
///     module_style::header,
///     // conditionally activate a global style
///     if active_tab == 0 { Some(my_style::active) } else { None }
///     // The same can be expressed with then_some:
///     (active_tab == 0).then_some(my_style::active)
/// );
/// ```
#[macro_export]
macro_rules! classes {
    () => { "" };
    ($($exp:expr),+$(,)?) => {
        ::stylance::JoinClasses::join_classes([$($exp.into()),*].as_slice())
    };
}