css-minify 0.5.2

CSS parser and minificator based on nom
Documentation
use crate::optimizations::transformer::Transform;
use crate::optimizations::{if_some_has_important, none_or_has_important};
use crate::structure::{Name, Parameters, Value};
use nom::lib::std::fmt::Formatter;
use std::fmt::Display;

#[derive(Clone, Debug, Default)]
pub struct Merge;

#[derive(Debug, Default, Clone)]
pub struct Margin(Option<Value>, Option<Value>, Option<Value>, Option<Value>);
#[derive(Debug, Default, Clone)]
pub struct Padding(Option<Value>, Option<Value>, Option<Value>, Option<Value>);

impl Transform for Merge {
    fn transform_parameters(&self, mut parameters: Parameters) -> Parameters {
        let mut margin = Margin::default();
        let mut padding = Padding::default();
        parameters.0.iter().for_each(|(name, val)| {
            if !parameters.0.contains_key("margin") {
                margin.add(name, val.clone());
            }
            if !parameters.0.contains_key("padding") {
                padding.add(name, val.clone());
            }
        });

        if margin.is_may_be_merged() {
            parameters.insert(String::from("margin"), margin.to_string());
            parameters.0.swap_remove("margin-top");
            parameters.0.swap_remove("margin-bottom");
            parameters.0.swap_remove("margin-left");
            parameters.0.swap_remove("margin-right");
        }
        if padding.is_may_be_merged() {
            parameters.insert(String::from("padding"), padding.to_string());
            parameters.0.swap_remove("padding-top");
            parameters.0.swap_remove("padding-bottom");
            parameters.0.swap_remove("padding-left");
            parameters.0.swap_remove("padding-right");
        }

        parameters
    }
}

impl Margin {
    fn add(&mut self, name: &Name, value: Value) -> bool {
        match name.as_str() {
            "margin-top" => {
                self.0 = Some(value);
                true
            }
            "margin-right" => {
                self.1 = Some(value);
                true
            }
            "margin-bottom" => {
                self.2 = Some(value);
                true
            }
            "margin-left" => {
                self.3 = Some(value);
                true
            }
            _ => false,
        }
    }

    fn is_may_be_merged(&self) -> bool {
        self.0.is_some()
            && self.1.is_some()
            && self.2.is_some()
            && self.3.is_some()
            && (self.all_elements_has_important() || self.no_one_element_has_no_important())
    }

    fn all_elements_has_important(&self) -> bool {
        if_some_has_important(self.0.as_ref())
            && if_some_has_important(self.1.as_ref())
            && if_some_has_important(self.2.as_ref())
            && if_some_has_important(self.3.as_ref())
    }

    fn no_one_element_has_no_important(&self) -> bool {
        !(none_or_has_important(self.0.as_ref())
            || none_or_has_important(self.1.as_ref())
            || none_or_has_important(self.2.as_ref())
            || none_or_has_important(self.3.as_ref()))
    }
}

impl Padding {
    fn add(&mut self, name: &Name, value: Value) -> bool {
        match name.as_str() {
            "padding-top" => {
                self.0 = Some(value);
                true
            }
            "padding-right" => {
                self.1 = Some(value);
                true
            }
            "padding-bottom" => {
                self.2 = Some(value);
                true
            }
            "padding-left" => {
                self.3 = Some(value);
                true
            }
            _ => false,
        }
    }

    fn is_may_be_merged(&self) -> bool {
        self.0.is_some()
            && self.1.is_some()
            && self.2.is_some()
            && self.3.is_some()
            && (self.all_elements_has_important() || self.no_one_element_has_no_important())
    }

    fn all_elements_has_important(&self) -> bool {
        if_some_has_important(self.0.as_ref())
            && if_some_has_important(self.1.as_ref())
            && if_some_has_important(self.2.as_ref())
            && if_some_has_important(self.3.as_ref())
    }

    fn no_one_element_has_no_important(&self) -> bool {
        !(none_or_has_important(self.0.as_ref())
            || none_or_has_important(self.1.as_ref())
            || none_or_has_important(self.2.as_ref())
            || none_or_has_important(self.3.as_ref()))
    }
}

impl Display for Margin {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let (top, right, bottom, left) = (
            self.0
                .clone()
                .map(|m| m.trim_end_matches("!important").trim().to_string())
                .unwrap_or_else(|| String::from("auto")),
            self.1
                .clone()
                .map(|m| m.trim_end_matches("!important").trim().to_string())
                .unwrap_or_else(|| String::from("auto")),
            self.2
                .clone()
                .map(|m| m.trim_end_matches("!important").trim().to_string())
                .unwrap_or_else(|| String::from("auto")),
            self.3
                .clone()
                .map(|m| m.trim_end_matches("!important").trim().to_string())
                .unwrap_or_else(|| String::from("auto")),
        );

        if top == bottom && right == left && top == right {
            write!(f, "{}", top)?;
        } else if top == bottom && right == left {
            write!(f, "{} {}", top, right)?;
        } else if right == left {
            write!(f, "{} {} {}", top, right, bottom)?;
        } else {
            write!(f, "{} {} {} {}", top, right, bottom, left)?;
        }

        if self.all_elements_has_important() {
            write!(f, "!important")?;
        }

        Ok(())
    }
}

impl Display for Padding {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let (top, right, bottom, left) = (
            self.0
                .clone()
                .map(|m| m.trim_end_matches("!important").trim().to_string())
                .unwrap_or_else(|| String::from("auto")),
            self.1
                .clone()
                .map(|m| m.trim_end_matches("!important").trim().to_string())
                .unwrap_or_else(|| String::from("auto")),
            self.2
                .clone()
                .map(|m| m.trim_end_matches("!important").trim().to_string())
                .unwrap_or_else(|| String::from("auto")),
            self.3
                .clone()
                .map(|m| m.trim_end_matches("!important").trim().to_string())
                .unwrap_or_else(|| String::from("auto")),
        );

        if top == bottom && right == left && top == right {
            write!(f, "{}", top)?;
        } else if top == bottom && right == left {
            write!(f, "{} {}", top, right)?;
        } else if right == left {
            write!(f, "{} {} {}", top, right, bottom)?;
        } else {
            write!(f, "{} {} {} {}", top, right, bottom, left)?;
        }

        if self.all_elements_has_important() {
            write!(f, "!important")?;
        }
        Ok(())
    }
}

#[cfg(test)]
mod test {
    use crate::optimizations::merge_m_n_p::Merge;
    use crate::optimizations::transformer::Transform;
    use crate::structure::{Block, Parameters, Selectors};
    use indexmap::map::IndexMap;

    #[test]
    fn test_full_compress() {
        assert_eq!(
            Merge::default().transform(
                Block {
                    selectors: Selectors::default(),
                    parameters: {
                        let mut map = IndexMap::new();
                        map.insert("margin-top".into(), "3px".into());
                        map.insert("margin-bottom".into(), "3px".into());
                        map.insert("margin-left".into(), "3px".into());
                        map.insert("margin-right".into(), "3px".into());
                        Parameters(map)
                    },
                }
                .into()
            ),
            Block {
                selectors: Selectors::default(),
                parameters: {
                    let mut map = IndexMap::new();
                    map.insert("margin".into(), "3px".into());
                    Parameters(map)
                },
            }
            .into()
        )
    }

    #[test]
    fn test_compress_2() {
        assert_eq!(
            Merge::default().transform(
                Block {
                    selectors: Selectors::default(),
                    parameters: {
                        let mut map = IndexMap::new();
                        map.insert("margin-top".into(), "3px".into());
                        map.insert("margin-bottom".into(), "3px".into());
                        map.insert("margin-left".into(), "4px".into());
                        map.insert("margin-right".into(), "4px".into());
                        Parameters(map)
                    },
                }
                .into()
            ),
            Block {
                selectors: Selectors::default(),
                parameters: {
                    let mut map = IndexMap::new();
                    map.insert("margin".into(), "3px 4px".into());
                    Parameters(map)
                },
            }
            .into()
        )
    }

    #[test]
    fn test_compress_3() {
        assert_eq!(
            Merge::default().transform(
                Block {
                    selectors: Selectors::default(),
                    parameters: {
                        let mut map = IndexMap::new();
                        map.insert("margin-top".into(), "3px".into());
                        map.insert("margin-bottom".into(), "1px".into());
                        map.insert("margin-left".into(), "4px".into());
                        map.insert("margin-right".into(), "4px".into());
                        Parameters(map)
                    },
                }
                .into()
            ),
            Block {
                selectors: Selectors::default(),
                parameters: {
                    let mut map = IndexMap::new();
                    map.insert("margin".into(), "3px 4px 1px".into());
                    Parameters(map)
                },
            }
            .into()
        )
    }

    #[test]
    fn test_compress_4() {
        assert_eq!(
            Merge::default().transform(
                Block {
                    selectors: Selectors::default(),
                    parameters: {
                        let mut map = IndexMap::new();
                        map.insert("margin-top".into(), "3px".into());
                        map.insert("margin-bottom".into(), "1px".into());
                        map.insert("margin-left".into(), "2px".into());
                        map.insert("margin-right".into(), "4px".into());
                        Parameters(map)
                    },
                }
                .into()
            ),
            Block {
                selectors: Selectors::default(),
                parameters: {
                    let mut map = IndexMap::new();
                    map.insert("margin".into(), "3px 4px 1px 2px".into());
                    Parameters(map)
                },
            }
            .into()
        )
    }
}