stylance 0.2.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 {
    pub use stylance_macros::*;

    pub struct NormalizeOptionStr<'a>(Option<&'a str>);

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

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

    impl<'a, T> From<Option<&'a T>> for NormalizeOptionStr<'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 NormalizeOptionStr<'a>
    where
        T: AsRef<str>,
    {
        fn from(value: &'a Option<T>) -> Self {
            Self(value.as_ref().map(AsRef::as_ref))
        }
    }

    pub fn normalize_option_str<'a>(value: impl Into<NormalizeOptionStr<'a>>) -> Option<&'a str> {
        value.into().0
    }

    pub fn join_opt_str_slice(slice: &[Option<&str>]) -> String {
        let mut iter = slice.iter().flat_map(|c| *c);
        let first = match iter.next() {
            Some(first) => first,
            None => return String::new(),
        };
        let size = iter.clone().map(|v| v.len()).sum::<usize>() + slice.len() - 1;
        let mut result = String::with_capacity(size);
        result.push_str(first);

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

/// 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!([pub] module_identifier, style_path);
/// ```
/// - 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!(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 {
    ($ident:ident, $str:expr) => {
        mod $ident {
            ::stylance::internal::import_style_classes_rel!($str);
        }
    };
    (pub $ident:ident, $str:expr) => {
        pub 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!([pub] module_identifier, style_path);
/// ```
/// - 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 {
    ($ident:ident, $str:expr) => {
        mod $ident {
            ::stylance::internal::import_style_classes!($str);
        }
    };
    (pub $ident:ident, $str:expr) => {
        pub 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;
}

macro_rules! impl_join_classes_for_tuples {
    (($($types:ident),*), ($($idx:tt),*)) => {
            impl<'a, $($types),*> JoinClasses for ($($types,)*)
            where
                $($types: Into<internal::NormalizeOptionStr<'a>>),*
            {
                fn join_classes(self) -> String {
                    let list = &[
                        $(internal::normalize_option_str(self.$idx)),*
                    ];
                    internal::join_opt_str_slice(list)
                }
            }
    };
}

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),*))
    };
}