css_minify/optimizations/
mod.rs

1mod color;
2mod font;
3mod merge_blocks;
4mod merge_m_n_p;
5mod merge_media;
6mod merge_shorthand;
7mod transformer;
8
9use crate::optimizations::color::optimize_color;
10use crate::optimizations::font::FontTransformer;
11use crate::optimizations::merge_blocks::MergeBlocks;
12use crate::optimizations::merge_m_n_p::Merge;
13use crate::optimizations::merge_media::MergeMedia;
14use crate::optimizations::merge_shorthand::MergeShortHand;
15use crate::optimizations::transformer::{Transform, Transformer, TransformerParameterFn};
16use crate::parsers::css_entity::parse_css;
17use crate::structure::Value;
18use derive_more::{From, Into};
19use nom::lib::std::fmt::Debug;
20use nom::lib::std::str::FromStr;
21use nom::{Err, Needed};
22use std::error::Error;
23use std::fmt::Display;
24use std::fmt::Formatter;
25
26/// Struct which stores all optimizations from css minify lib
27pub struct Minifier {
28    transformer: Transformer,
29    merge_m_n_p: Merge,
30    merge_shorthand: MergeShortHand,
31    media: MergeMedia,
32    blocks: MergeBlocks,
33    font: FontTransformer,
34}
35
36impl Minifier {
37    /// Minify css input and return result with minified css string
38    pub fn minify<'a>(&self, input: &'a str, level: Level) -> MResult<'a> {
39        let mut result = parse_css(input)
40            .map(|(_, blocks)| blocks)
41            .map_err(|e| MError(input, e));
42
43        if level == Level::Three {
44            result = result
45                .map(|blocks| self.blocks.transform_many(blocks))
46                .map(|blocks| self.media.transform_many(blocks))
47        }
48
49        if level >= Level::Two {
50            result = result
51                .map(|blocks| self.merge_m_n_p.transform_many(blocks))
52                .map(|blocks| self.merge_shorthand.transform_many(blocks))
53        }
54
55        if level >= Level::One {
56            result = result
57                .map(|blocks| self.transformer.transform_many(blocks))
58                .map(|blocks| self.font.transform_many(blocks))
59        }
60
61        result.map(|blocks| blocks.to_string())
62    }
63}
64
65impl Default for Minifier {
66    fn default() -> Self {
67        let mut transformer = Transformer::default();
68        transformer.register_parameter(TransformerParameterFn::Value(Box::new(|value| {
69            optimize_color(&value).into()
70        })));
71        transformer.register_parameter(TransformerParameterFn::Value(Box::new(|mut value| {
72            if value.starts_with("0px") {
73                value = format!("0{}", value.trim_start_matches("0px"))
74            }
75            if value.starts_with("0rem") {
76                value = format!("0{}", value.trim_start_matches("0rem"))
77            }
78            if value.starts_with("0.") {
79                value = format!(".{}", value.trim_start_matches("0."))
80            }
81            value
82                .replace(" 0px", " 0")
83                .replace(" 0rem", " 0")
84                .replace(" 0.", " .")
85                .replace(", ", ",")
86                .replace(" !important", "!important")
87        })));
88
89        transformer.register_parameter(TransformerParameterFn::Name(Box::new(|name| {
90            name.to_lowercase()
91        })));
92
93        let merge_m_n_p = Merge;
94        let merge_shorthand = MergeShortHand;
95        let media = MergeMedia;
96        let blocks = MergeBlocks;
97        let font = FontTransformer;
98
99        Minifier {
100            merge_m_n_p,
101            merge_shorthand,
102            transformer,
103            media,
104            blocks,
105            font,
106        }
107    }
108}
109
110/// Transforming level
111#[derive(Ord, PartialOrd, Eq, PartialEq, Debug, Copy, Clone)]
112pub enum Level {
113    /// Disable transformer
114    Zero = 0,
115    /// Remove whitespaces, replace `0.` to `.` and others non dangerous optimizations
116    /// It's default level
117    One = 1,
118    /// Level One + shortcuts (margins, paddings, backgrounds and etc)
119    /// In mostly cases it's non dangerous optimizations, but be careful
120    Two = 2,
121    /// Level Two + merge @media and css blocks with equal screen/selectors
122    /// It is a danger optimizations, because ordering of your css code may be changed
123    Three = 3,
124}
125
126impl Default for Level {
127    fn default() -> Self {
128        Self::One
129    }
130}
131
132impl FromStr for Level {
133    type Err = ParseLevelError;
134
135    fn from_str(s: &str) -> Result<Self, Self::Err> {
136        match s {
137            "0" => Ok(Level::Zero),
138            "1" => Ok(Level::One),
139            "2" => Ok(Level::Two),
140            "3" => Ok(Level::Three),
141            _ => Err(ParseLevelError),
142        }
143    }
144}
145
146#[derive(Default, Copy, Clone)]
147pub struct ParseLevelError;
148
149impl Display for ParseLevelError {
150    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
151        write!(f, "Input must be number from 0 to 3 values")
152    }
153}
154
155impl Debug for ParseLevelError {
156    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
157        f.debug_struct("ParseLevelError")
158            .field(
159                "message",
160                &"Input must be number from 0 to 3 values".to_string(),
161            )
162            .finish()
163    }
164}
165
166impl Error for ParseLevelError {}
167
168pub type MResult<'a> = Result<String, MError<'a>>;
169
170#[derive(From, Into, PartialEq)]
171pub struct MError<'a>(&'a str, nom::Err<nom::error::Error<&'a str>>);
172
173impl Debug for MError<'_> {
174    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
175        f.debug_struct("MError")
176            .field("message", &format!("{}", self))
177            .field("error", &format!("{:?}", self.0))
178            .finish()
179    }
180}
181
182impl Display for MError<'_> {
183    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
184        let unparsed_size = match &self.1 {
185            Err::Incomplete(n) => match n {
186                Needed::Unknown => None,
187                Needed::Size(s) => Some(s.get()),
188            },
189            Err::Error(e) => Some(e.input.len()),
190            Err::Failure(e) => Some(e.input.len()),
191        };
192        if let Some(size) = unparsed_size {
193            let parsed_css = &self.0[..size];
194            let (count, last_line) = parsed_css.lines().enumerate().last().unwrap();
195
196            write!(f, "Invalid block at line {}:{}", count + 1, last_line.len())
197        } else {
198            write!(f, "Invalid css")
199        }
200    }
201}
202
203impl<'a> Error for MError<'a> {}
204
205#[inline]
206pub(crate) fn if_some_has_important(input: Option<&Value>) -> bool {
207    if let Some(input) = input {
208        return input.ends_with("!important");
209    }
210    true
211}
212
213#[inline]
214pub(crate) fn none_or_has_important(input: Option<&Value>) -> bool {
215    if let Some(input) = input {
216        return input.ends_with("!important");
217    }
218    false
219}
220
221#[cfg(test)]
222mod test {
223    use crate::optimizations::{Level, Minifier};
224
225    #[test]
226    fn test_minify() {
227        assert_eq!(
228            Minifier::default().minify(
229                r#"
230                #some_id, input {
231                    padding: 5px 3px; /* Mega comment */
232                    color: white;
233                }
234                
235                
236                /* this is are test id */
237                #some_id_2, .class {
238                    padding: 5px 4px; /* Mega comment */
239                    Color: rgb(255, 255, 255);
240                    font-weight: bold;
241                }
242            "#,
243                Level::Three,
244            ),
245            Ok("#some_id,input{padding:5px 3px;color:white}#some_id_2,.class{padding:5px 4px;color:#fff;font-weight:700}".into())
246        )
247    }
248
249    #[test]
250    fn test_minify_invalid_css() {
251        assert_eq!(
252            Minifier::default()
253                .minify(
254                    r#"
255                #some_id, input {
256                    padding: 5px 3px; /* Mega comment */
257                    color: white;
258                }} /* sasd */
259                
260                
261                /* this is are test id */
262                #some_id_2, .class {
263                    padding: 5px 4px; /* Mega comment */
264                    Color: rgb(255, 255, 255);
265                }
266            "#,
267                    Level::Three,
268                )
269                .unwrap_err()
270                .to_string(),
271            "Invalid block at line 9:28"
272        )
273    }
274
275    #[test]
276    fn test_block_with_modificator_selector() {
277        assert_eq!(
278            Minifier::default().minify(
279                r#"
280                    p:not(.classy) {
281                        color: red;
282                    }
283                    "#,
284                Level::Three,
285            ),
286            Ok("p:not(.classy){color:red}".into())
287        )
288    }
289
290    #[test]
291    fn test_block_with_selector_and_pc() {
292        assert_eq!(
293            Minifier::default().minify(
294                r#"
295                    @-ms-viewport {
296                      width: device-width;
297                    }
298                    "#,
299                Level::Three,
300            ),
301            Ok("@-ms-viewport {width:device-width}".into())
302        )
303    }
304
305    #[test]
306    fn test_block_with_invalid_color() {
307        assert_eq!(
308            Minifier::default().minify("*{color:#f}", Level::Three),
309            Ok("*{color:#f}".into())
310        )
311    }
312}