css_minify/optimizations/
merge_shorthand.rs

1use crate::optimizations::transformer::Transform;
2use crate::optimizations::{if_some_has_important, none_or_has_important};
3use crate::structure::{Name, Parameters, Value};
4use nom::lib::std::fmt::Formatter;
5use std::fmt::Display;
6
7#[derive(Default, Debug, Clone)]
8pub struct MergeShortHand;
9
10impl Transform for MergeShortHand {
11    fn transform_parameters(&self, mut parameters: Parameters) -> Parameters {
12        let mut font = FontShortHand::default();
13        let mut list = ListShortHand::default();
14        let mut background = BackgroundShortHand::default();
15        let mut border = BorderShortHand::default();
16        let mut outline = OutlineShortHand::default();
17        let mut transition = TransitionShortHand::default();
18
19        parameters
20            .0
21            .iter()
22            .for_each(|(name, value): (&Name, &Value)| {
23                if !parameters.0.contains_key("font") {
24                    font.add(name, value.clone());
25                }
26                if !parameters.0.contains_key("list-style") {
27                    list.add(name, value.clone());
28                }
29                if !parameters.0.contains_key("background") {
30                    background.add(name, value.clone());
31                }
32                if !parameters.0.contains_key("border") {
33                    border.add(name, value.clone());
34                }
35                if !parameters.0.contains_key("outline") {
36                    outline.add(name, value.clone());
37                }
38                if !parameters.0.contains_key("transition") {
39                    transition.add(name, value.clone());
40                }
41            });
42
43        if font.is_maybe_shorted() {
44            parameters
45                .0
46                .insert(String::from("font"), font.to_string().trim().to_string());
47            parameters.0.swap_remove("font-style");
48            parameters.0.swap_remove("font-variant");
49            parameters.0.swap_remove("font-weight");
50            parameters.0.swap_remove("font-size");
51            parameters.0.swap_remove("line-height");
52            parameters.0.swap_remove("font-family");
53        }
54
55        if list.is_maybe_shorted() {
56            parameters.0.insert(
57                String::from("list-style"),
58                list.to_string().trim().to_string(),
59            );
60            parameters.0.swap_remove("list-style-type");
61            parameters.0.swap_remove("list-style-position");
62            parameters.0.swap_remove("list-style-image");
63        }
64
65        if background.is_maybe_shorted() {
66            parameters.0.insert(
67                String::from("background"),
68                background.to_string().trim().to_string(),
69            );
70            parameters.0.swap_remove("background-attachment");
71            parameters.0.swap_remove("background-color");
72            parameters.0.swap_remove("background-position");
73            parameters.0.swap_remove("background-repeat");
74            parameters.0.swap_remove("background-image");
75        }
76
77        if border.is_maybe_shorted() {
78            parameters.0.insert(
79                String::from("border"),
80                border.to_string().trim().to_string(),
81            );
82            parameters.0.swap_remove("border-width");
83            parameters.0.swap_remove("border-style");
84            parameters.0.swap_remove("border-color");
85        }
86
87        if outline.is_maybe_shorted() {
88            parameters.0.insert(
89                String::from("outline"),
90                outline.to_string().trim().to_string(),
91            );
92            parameters.0.swap_remove("outline-width");
93            parameters.0.swap_remove("outline-style");
94            parameters.0.swap_remove("outline-color");
95        }
96
97        if transition.is_maybe_shorted() {
98            parameters.0.insert(
99                String::from("transition"),
100                transition.to_string().trim().to_string(),
101            );
102            parameters.0.swap_remove("transition-property");
103            parameters.0.swap_remove("transition-duration");
104            parameters.0.swap_remove("transition-delay");
105            parameters.0.swap_remove("transition-timing-function");
106        }
107
108        parameters
109    }
110}
111
112#[derive(Debug, Default)]
113struct FontShortHand {
114    font_style: Option<Value>,
115    font_variant: Option<Value>,
116    font_weight: Option<Value>,
117    font_size: Option<Value>,
118    line_height: Option<Value>,
119    font_family: Option<Value>,
120}
121
122impl FontShortHand {
123    fn is_maybe_shorted(&self) -> bool {
124        self.font_size.is_some()
125            && self.font_family.is_some()
126            && (self.all_elements_has_important() || self.no_one_element_has_no_important())
127    }
128
129    fn all_elements_has_important(&self) -> bool {
130        if_some_has_important(self.font_style.as_ref())
131            && if_some_has_important(self.font_variant.as_ref())
132            && if_some_has_important(self.font_weight.as_ref())
133            && if_some_has_important(self.font_size.as_ref())
134            && if_some_has_important(self.line_height.as_ref())
135            && if_some_has_important(self.font_family.as_ref())
136    }
137
138    fn no_one_element_has_no_important(&self) -> bool {
139        !(none_or_has_important(self.font_style.as_ref())
140            || none_or_has_important(self.font_variant.as_ref())
141            || none_or_has_important(self.font_weight.as_ref())
142            || none_or_has_important(self.font_size.as_ref())
143            || none_or_has_important(self.line_height.as_ref())
144            || none_or_has_important(self.font_family.as_ref()))
145    }
146
147    fn add(&mut self, name: &Name, value: Value) {
148        match name.as_str() {
149            "font-style" => self.font_style = Some(value),
150            "font-variant" => self.font_variant = Some(value),
151            "font-weight" => self.font_weight = Some(value),
152            "font-size" => self.font_size = Some(value),
153            "line-height" => self.line_height = Some(value),
154            "font-family" => self.font_family = Some(value),
155            _ => {}
156        }
157    }
158}
159
160impl Display for FontShortHand {
161    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
162        if let Some(v) = &self.font_style {
163            write!(f, "{}", v.trim_end_matches("!important").trim())?;
164        }
165        if let Some(v) = &self.font_variant {
166            write!(f, " {}", v.trim_end_matches("!important").trim())?;
167        }
168        if let Some(v) = &self.font_weight {
169            write!(f, " {}", v.trim_end_matches("!important").trim())?;
170        }
171        if let Some(v) = &self.font_size {
172            write!(f, " {}", v.trim_end_matches("!important").trim())?;
173        }
174        if let Some(v) = &self.line_height {
175            write!(f, "/{}", v.trim_end_matches("!important").trim())?;
176        }
177        if let Some(v) = &self.font_family {
178            write!(f, " {}", v.trim_end_matches("!important").trim())?;
179        }
180        if self.all_elements_has_important() {
181            write!(f, "!important")?;
182        }
183        Ok(())
184    }
185}
186
187#[derive(Debug, Default)]
188struct ListShortHand {
189    list_style_type: Option<Value>,
190    list_style_position: Option<Value>,
191    list_style_image: Option<Value>,
192}
193
194impl ListShortHand {
195    fn is_maybe_shorted(&self) -> bool {
196        (self.list_style_type.is_some()
197            || self.list_style_position.is_some()
198            || self.list_style_image.is_some())
199            && (self.all_elements_has_important() || self.no_one_element_has_no_important())
200    }
201
202    fn all_elements_has_important(&self) -> bool {
203        if_some_has_important(self.list_style_type.as_ref())
204            && if_some_has_important(self.list_style_position.as_ref())
205            && if_some_has_important(self.list_style_image.as_ref())
206    }
207
208    fn no_one_element_has_no_important(&self) -> bool {
209        !(none_or_has_important(self.list_style_type.as_ref())
210            || none_or_has_important(self.list_style_position.as_ref())
211            || none_or_has_important(self.list_style_image.as_ref()))
212    }
213
214    fn add(&mut self, name: &Name, value: Value) {
215        match name.as_str() {
216            "list-style-type" => self.list_style_type = Some(value),
217            "list-style-position" => self.list_style_position = Some(value),
218            "list-style-image" => self.list_style_image = Some(value),
219            _ => {}
220        }
221    }
222}
223
224impl Display for ListShortHand {
225    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
226        if let Some(v) = &self.list_style_type {
227            write!(f, "{}", v.trim_end_matches("!important").trim())?;
228        }
229        if let Some(v) = &self.list_style_position {
230            write!(f, " {}", v.trim_end_matches("!important").trim())?;
231        }
232        if let Some(v) = &self.list_style_image {
233            write!(f, " {}", v.trim_end_matches("!important").trim())?;
234        }
235        if self.all_elements_has_important() {
236            write!(f, "!important")?;
237        }
238        Ok(())
239    }
240}
241
242#[derive(Debug, Default)]
243struct BackgroundShortHand {
244    background_color: Option<Value>,
245    background_image: Option<Value>,
246    background_repeat: Option<Value>,
247    background_attachment: Option<Value>,
248    background_position: Option<Value>,
249}
250
251impl BackgroundShortHand {
252    fn is_maybe_shorted(&self) -> bool {
253        (self.background_color.is_some()
254            || self.background_image.is_some()
255            || self.background_repeat.is_some()
256            || self.background_attachment.is_some()
257            || self.background_position.is_some())
258            && (self.all_elements_has_important() || self.no_one_element_has_no_important())
259    }
260
261    fn all_elements_has_important(&self) -> bool {
262        if_some_has_important(self.background_color.as_ref())
263            && if_some_has_important(self.background_image.as_ref())
264            && if_some_has_important(self.background_repeat.as_ref())
265            && if_some_has_important(self.background_attachment.as_ref())
266            && if_some_has_important(self.background_position.as_ref())
267    }
268
269    fn no_one_element_has_no_important(&self) -> bool {
270        !(none_or_has_important(self.background_color.as_ref())
271            || none_or_has_important(self.background_image.as_ref())
272            || none_or_has_important(self.background_repeat.as_ref())
273            || none_or_has_important(self.background_attachment.as_ref())
274            || none_or_has_important(self.background_position.as_ref()))
275    }
276
277    fn add(&mut self, name: &Name, value: Value) {
278        match name.as_str() {
279            "background-color" => self.background_color = Some(value),
280            "background-image" => self.background_image = Some(value),
281            "background-repeat" => self.background_repeat = Some(value),
282            "background-attachment" => self.background_attachment = Some(value),
283            "background-position" => self.background_position = Some(value),
284            _ => {}
285        }
286    }
287}
288
289impl Display for BackgroundShortHand {
290    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
291        if let Some(v) = &self.background_color {
292            write!(f, " {}", v.trim_end_matches("!important").trim())?;
293        }
294        if let Some(v) = &self.background_image {
295            write!(f, " {}", v.trim_end_matches("!important").trim())?;
296        }
297        if let Some(v) = &self.background_repeat {
298            write!(f, " {}", v.trim_end_matches("!important").trim())?;
299        }
300        if let Some(v) = &self.background_attachment {
301            write!(f, "{}", v.trim_end_matches("!important").trim())?;
302        }
303        if let Some(v) = &self.background_position {
304            write!(f, " {}", v.trim_end_matches("!important").trim())?;
305        }
306        if self.all_elements_has_important() {
307            write!(f, "!important")?;
308        }
309
310        Ok(())
311    }
312}
313
314#[derive(Debug, Default)]
315struct BorderShortHand {
316    border_width: Option<Value>,
317    border_style: Option<Value>,
318    border_color: Option<Value>,
319}
320
321impl BorderShortHand {
322    fn is_maybe_shorted(&self) -> bool {
323        (self.border_width.is_some() || self.border_style.is_some() || self.border_color.is_some())
324            && (self.all_elements_has_important() || self.no_one_element_has_no_important())
325    }
326
327    fn all_elements_has_important(&self) -> bool {
328        if_some_has_important(self.border_width.as_ref())
329            && if_some_has_important(self.border_style.as_ref())
330            && if_some_has_important(self.border_color.as_ref())
331    }
332
333    fn no_one_element_has_no_important(&self) -> bool {
334        !(none_or_has_important(self.border_width.as_ref())
335            || none_or_has_important(self.border_style.as_ref())
336            || none_or_has_important(self.border_color.as_ref()))
337    }
338
339    fn add(&mut self, name: &Name, value: Value) {
340        match name.as_str() {
341            "border-width" => self.border_width = Some(value),
342            "border-style" => self.border_style = Some(value),
343            "border-color" => self.border_color = Some(value),
344            _ => {}
345        }
346    }
347}
348
349impl Display for BorderShortHand {
350    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
351        if let Some(v) = &self.border_width {
352            write!(f, "{}", v.trim_end_matches("!important").trim())?;
353        }
354        if let Some(v) = &self.border_style {
355            write!(f, " {}", v.trim_end_matches("!important").trim())?;
356        }
357        if let Some(v) = &self.border_color {
358            write!(f, " {}", v.trim_end_matches("!important").trim())?;
359        }
360        if self.all_elements_has_important() {
361            write!(f, "!important")?;
362        }
363
364        Ok(())
365    }
366}
367
368#[derive(Debug, Default)]
369struct OutlineShortHand {
370    outline_width: Option<Value>,
371    outline_style: Option<Value>,
372    outline_color: Option<Value>,
373}
374
375impl OutlineShortHand {
376    fn is_maybe_shorted(&self) -> bool {
377        (self.outline_width.is_some()
378            || self.outline_style.is_some()
379            || self.outline_color.is_some())
380            && (self.all_elements_has_important() || self.no_one_element_has_no_important())
381    }
382
383    fn all_elements_has_important(&self) -> bool {
384        if_some_has_important(self.outline_width.as_ref())
385            && if_some_has_important(self.outline_style.as_ref())
386            && if_some_has_important(self.outline_color.as_ref())
387    }
388
389    fn no_one_element_has_no_important(&self) -> bool {
390        !(none_or_has_important(self.outline_width.as_ref())
391            || none_or_has_important(self.outline_style.as_ref())
392            || none_or_has_important(self.outline_color.as_ref()))
393    }
394
395    fn add(&mut self, name: &Name, value: Value) {
396        match name.as_str() {
397            "outline-width" => self.outline_width = Some(value),
398            "outline-style" => self.outline_style = Some(value),
399            "outline-color" => self.outline_color = Some(value),
400            _ => {}
401        }
402    }
403}
404
405impl Display for OutlineShortHand {
406    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
407        if let Some(v) = &self.outline_width {
408            write!(f, "{}", v.trim_end_matches("!important").trim())?;
409        }
410        if let Some(v) = &self.outline_style {
411            write!(f, " {}", v.trim_end_matches("!important").trim())?;
412        }
413        if let Some(v) = &self.outline_color {
414            write!(f, " {}", v.trim_end_matches("!important").trim())?;
415        }
416        if self.all_elements_has_important() {
417            write!(f, "!important")?;
418        }
419
420        Ok(())
421    }
422}
423
424#[derive(Debug, Default)]
425struct TransitionShortHand {
426    transition_property: Option<Value>,
427    transition_duration: Option<Value>,
428    transition_delay: Option<Value>,
429    transition_timing_function: Option<Value>,
430}
431
432impl TransitionShortHand {
433    fn is_maybe_shorted(&self) -> bool {
434        (self.transition_property.is_some()
435            || self.transition_duration.is_some()
436            || self.transition_delay.is_some()
437            || self.transition_timing_function.is_some())
438            && (self.all_elements_has_important() || self.no_one_element_has_no_important())
439    }
440
441    fn all_elements_has_important(&self) -> bool {
442        if_some_has_important(self.transition_property.as_ref())
443            && if_some_has_important(self.transition_duration.as_ref())
444            && if_some_has_important(self.transition_delay.as_ref())
445            && if_some_has_important(self.transition_timing_function.as_ref())
446    }
447
448    fn no_one_element_has_no_important(&self) -> bool {
449        !(none_or_has_important(self.transition_property.as_ref())
450            || none_or_has_important(self.transition_duration.as_ref())
451            || none_or_has_important(self.transition_delay.as_ref())
452            || none_or_has_important(self.transition_timing_function.as_ref()))
453    }
454
455    fn add(&mut self, name: &Name, value: Value) {
456        match name.as_str() {
457            "transition-property" => self.transition_property = Some(value),
458            "transition-duration" => self.transition_duration = Some(value),
459            "transition-delay" => self.transition_delay = Some(value),
460            "transition-timing-function" => self.transition_timing_function = Some(value),
461            _ => {}
462        }
463    }
464}
465
466impl Display for TransitionShortHand {
467    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
468        if let Some(v) = &self.transition_property {
469            write!(f, "{}", v.trim_end_matches("!important").trim())?;
470        }
471        if let Some(v) = &self.transition_duration {
472            write!(f, " {}", v.trim_end_matches("!important").trim())?;
473        }
474        if let Some(v) = &self.transition_delay {
475            write!(f, " {}", v.trim_end_matches("!important").trim())?;
476        }
477        if let Some(v) = &self.transition_timing_function {
478            write!(f, " {}", v.trim_end_matches("!important").trim())?;
479        }
480        if self.all_elements_has_important() {
481            write!(f, "!important")?;
482        }
483
484        Ok(())
485    }
486}
487
488#[cfg(test)]
489mod test {
490    use crate::optimizations::merge_shorthand::MergeShortHand;
491    use crate::optimizations::transformer::Transform;
492    use crate::structure::{Block, Parameters, Selectors};
493    use indexmap::map::IndexMap;
494
495    #[test]
496    fn test_compress_font() {
497        assert_eq!(
498            MergeShortHand::default().transform(
499                Block {
500                    selectors: Selectors::default(),
501                    parameters: {
502                        let mut map = IndexMap::new();
503                        map.insert("font-style".into(), "italic".into());
504                        map.insert("font-weight".into(), "bold".into());
505                        map.insert("font-size".into(), ".8em".into());
506                        map.insert("line-height".into(), "1.2".into());
507                        map.insert("font-family".into(), "Arial, sans-serif".into());
508                        Parameters(map)
509                    },
510                }
511                .into()
512            ),
513            Block {
514                selectors: Selectors::default(),
515                parameters: {
516                    let mut map = IndexMap::new();
517                    map.insert(
518                        "font".into(),
519                        "italic bold .8em/1.2 Arial, sans-serif".into(),
520                    );
521                    Parameters(map)
522                },
523            }
524            .into()
525        )
526    }
527
528    #[test]
529    fn test_compress_background() {
530        assert_eq!(
531            MergeShortHand::default().transform(
532                Block {
533                    selectors: Selectors::default(),
534                    parameters: {
535                        let mut map = IndexMap::new();
536                        map.insert("background-color".into(), "#000".into());
537                        map.insert("background-image".into(), "url(images/bg.gif)".into());
538                        map.insert("background-repeat".into(), "no-repeat".into());
539                        map.insert("background-position".into(), "left top".into());
540                        Parameters(map)
541                    },
542                }
543                .into()
544            ),
545            Block {
546                selectors: Selectors::default(),
547                parameters: {
548                    let mut map = IndexMap::new();
549                    map.insert(
550                        "background".into(),
551                        "#000 url(images/bg.gif) no-repeat left top".into(),
552                    );
553                    Parameters(map)
554                },
555            }
556            .into()
557        )
558    }
559
560    #[test]
561    fn test_background_important() {
562        assert_eq!(
563            MergeShortHand::default().transform(
564                Block {
565                    selectors: Selectors::default(),
566                    parameters: {
567                        let mut map = IndexMap::new();
568                        map.insert("background-color".into(), "#000 !important".into());
569                        Parameters(map)
570                    },
571                }
572                .into()
573            ),
574            Block {
575                selectors: Selectors::default(),
576                parameters: {
577                    let mut map = IndexMap::new();
578                    map.insert("background".into(), "#000!important".into());
579                    Parameters(map)
580                },
581            }
582            .into()
583        )
584    }
585
586    #[test]
587    fn test_compress_border() {
588        assert_eq!(
589            MergeShortHand::default().transform(
590                Block {
591                    selectors: Selectors::default(),
592                    parameters: {
593                        let mut map = IndexMap::new();
594                        map.insert("border-width".into(), "1px".into());
595                        map.insert("border-style".into(), "solid".into());
596                        map.insert("border-color".into(), "#000".into());
597                        Parameters(map)
598                    },
599                }
600                .into()
601            ),
602            Block {
603                selectors: Selectors::default(),
604                parameters: {
605                    let mut map = IndexMap::new();
606                    map.insert("border".into(), "1px solid #000".into());
607                    Parameters(map)
608                },
609            }
610            .into()
611        )
612    }
613
614    #[test]
615    fn test_compress_outline() {
616        assert_eq!(
617            MergeShortHand::default().transform(
618                Block {
619                    selectors: Selectors::default(),
620                    parameters: {
621                        let mut map = IndexMap::new();
622                        map.insert("outline-width".into(), "1px".into());
623                        map.insert("outline-style".into(), "solid".into());
624                        map.insert("outline-color".into(), "#000".into());
625                        Parameters(map)
626                    },
627                }
628                .into()
629            ),
630            Block {
631                selectors: Selectors::default(),
632                parameters: {
633                    let mut map = IndexMap::new();
634                    map.insert("outline".into(), "1px solid #000".into());
635                    Parameters(map)
636                },
637            }
638            .into()
639        )
640    }
641}