css_minify/optimizations/
mod.rs1mod 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
26pub 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 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#[derive(Ord, PartialOrd, Eq, PartialEq, Debug, Copy, Clone)]
112pub enum Level {
113 Zero = 0,
115 One = 1,
118 Two = 2,
121 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}