static_web_minify/
lib.rs

1//! Include a minified css, js, html file as an inline const in your high-performance compiled web
2//! application. Inspired by [const-css-minify](https://github.com/scpso/const-css-minify)
3//!
4//! Unlinke `include_str!()`, you must put the name file from root of your rust project (see
5//! [issue 54725](https://github.com/rust-lang/rust/issues/54725)).
6//!
7//! ```rust
8//! use static_web_minify::minify_js_file;
9//!
10//! // this is probably the pattern you want to use
11//! const JS: &str = minify_js_file!("tests/test.js");
12//! ```
13//!
14//! It's also possible to include a raw string with:
15//! ```rust
16//! use static_web_minify::minify_js_str;
17//!
18//! const JS: &str = minify_js_str!(r#"
19//!     function my_func() {
20//!         // Great !
21//!     }
22//! "#);
23//! assert_eq!(JS, "var my_func=(()=>{})");
24//! ```
25//!
26//! This project has dependencies to [css-minify](https://crates.io/crates/css-minify), [minify-js](https://crates.io/crates/minify-js), [html-minifier](https://crates.io/crates/html-minifier), [flate2](https://crates.io/crates/flate2).
27use proc_macro::TokenStream;
28use std::str::FromStr;
29use css_minify::optimizations::{Minifier, Level};
30use std::fs;
31use std::path::Path;
32use minify_js::{Session, TopLevelMode, minify};
33use html_minifier::HTMLMinifier;
34use std::io::prelude::*;
35use flate2::Compression;
36use flate2::write::ZlibEncoder;
37
38/// Type to tranforme data sting or file name in data
39type DataFunction = fn(&str) -> String;
40
41/// Control that only one input is give to macro
42fn parse_only_one_input(input: TokenStream, fct_name: &str) -> String {
43    let tokens: Vec<_> = input.into_iter().collect();
44
45    if tokens.len() != 1 {
46        panic!("{} requires only a single str as input!", fct_name);
47    }
48
49    match tokens.first() {
50        Some(f) => parse_filemane_or_code_input(f.to_string()),
51        None => {
52            panic!("{} requires only a single str as input!", fct_name);
53        }
54    }
55}
56
57/// Control that only one input is give to macro
58fn parse_only_two_input(input: TokenStream, fct_name: &str) -> (String, String) {
59    let tokens: Vec<_> = input.into_iter().collect();
60
61    if tokens.len() != 3 { // 3 -> we have a comma
62        panic!("{} requires two arguments. First is str as input!", fct_name);
63    }
64
65    let varname = match tokens.first() {
66        Some(f) => match parse_varname_input(f.to_string()) {
67            Some(f) => f,
68            None => {
69                panic!("{} first argument requiere a const/static name", fct_name)
70            }
71        },
72        None => {
73            panic!("{} requires two arguments. First is str as input!", fct_name)
74        }
75    };
76
77    let comma = match tokens.get(1) {
78        Some(f) => f.to_string(),
79        None => ",".to_string()
80    };
81
82    if comma != "," {
83        panic!("{} requires two arguments, separated by comma!", fct_name);
84    }
85
86    let filename = match tokens.last() {
87        Some(f) => parse_filemane_or_code_input(f.to_string()),
88        None => {
89            panic!("{} requires two arguments. First is str as input!", fct_name)
90        }
91    };
92
93    (filename, varname)
94}
95
96/// Extra string
97fn parse_filemane_or_code_input(mut first_token: String) -> String {
98    let data_len = first_token.len();
99
100    // Now check if start by is " or r#"
101    if first_token.starts_with("r#\"") {
102        first_token = first_token[3..data_len - 2].to_string();
103    } else {
104        first_token = first_token[1..data_len - 1].to_string();
105    }
106
107    first_token
108}
109
110/// Extract var name
111fn parse_varname_input(token: String) -> Option<String> {
112    for c in token.chars() {
113        match c {
114            'a'..='z' => {},
115            'A'..='Z' => {},
116            '0'..='9' => {},
117            '_' => {},
118            _ => return None
119        }
120    }
121
122    Some(token)
123}
124
125// Minify a CSS stream.
126fn minify_css(fct_name: &str, input: TokenStream, f: DataFunction) -> TokenStream {
127    let argument = parse_only_one_input(input, fct_name);
128
129    let data_to_minimize = f(&argument);
130
131    let result =  Minifier::default().minify(&data_to_minimize, Level::One);
132
133    if let Err(e) = result {
134        panic!("{} has an issue to minimify CSS string: '{}', {}", fct_name, e, data_to_minimize);
135    }
136
137    let minified = format!( "r####\"{}\"####", result.unwrap());
138    
139    TokenStream::from_str(&minified).unwrap()
140}
141
142/// Produce a minified css from string.
143///
144/// ```rust
145/// use static_web_minify::minify_css_str;
146///
147/// const CSS: &str = minify_css_str!(r#"
148///     body {
149///         color: #fff;
150///     }
151/// "#);
152/// 
153/// assert_eq!(CSS, "body{color:#fff}");
154/// ```
155///
156/// ```rust
157/// use static_web_minify::minify_css_str;
158///
159/// const CSS: &str = minify_css_str!("
160///     body {
161///         color: #fff;
162///     }
163/// ");
164/// 
165/// assert_eq!(CSS, "body{color:#fff}");
166/// ```
167#[proc_macro]
168pub fn minify_css_str(input: TokenStream) -> TokenStream {
169    minify_css("minify_css_str", input, |s| s.to_string())
170}
171
172/// Produce a minified css from file.
173///
174/// ```rust
175/// use static_web_minify::minify_css_file;
176///
177/// const CSS: &str = minify_css_file!("tests/test.css");
178/// assert_eq!(CSS, "body{color:#fff}");
179/// ```
180#[proc_macro]
181pub fn minify_css_file(input: TokenStream) -> TokenStream {
182    minify_css("minify_css_file", input, |filename|{
183        match fs::read_to_string(Path::new(filename)) {
184            Ok(s) => s,
185            Err(e) => panic!("minify_css_file error when reading file {}: {}", filename, &e),
186        }
187    })
188}
189
190// Minify a JS stream.
191fn minify_js(fct_name: &str, input: TokenStream, f: DataFunction) -> TokenStream {
192    let argument = parse_only_one_input(input, fct_name);
193
194    let data_to_minimize = f(&argument);
195
196    let code = data_to_minimize.as_bytes();
197    let session = Session::new();
198    let mut out = Vec::new();
199
200    if let Err(e) = minify(&session, TopLevelMode::Global, code, &mut out) {
201        panic!("{} has an issue to minimify string: '{}', {}", fct_name, e, data_to_minimize);
202    }
203
204    let minified = format!( "r####\"{}\"####", String::from_utf8_lossy(&out));
205    
206    TokenStream::from_str(&minified).unwrap()
207}
208
209/// Produce a minified js from string.
210///
211/// ```rust
212/// use static_web_minify::minify_js_str;
213///
214/// const JS: &str = minify_js_str!(r#"
215///     function my_func() {
216///         // Great !
217///     }
218/// "#);
219/// 
220/// assert_eq!(JS, "var my_func=(()=>{})");
221/// ```
222///
223/// ```rust
224/// use static_web_minify::minify_js_str;
225///
226/// const JS: &str = minify_js_str!("
227///     function my_func() {
228///         // Great !
229///     }
230/// ");
231/// 
232/// assert_eq!(JS, "var my_func=(()=>{})");
233/// ```
234#[proc_macro]
235pub fn minify_js_str(input: TokenStream) -> TokenStream {
236    minify_js("minify_js_str", input, |s| s.to_string())
237}
238
239/// Produce a minified js from file.
240///
241/// ```rust
242/// use static_web_minify::minify_js_file;
243///
244/// const JS: &str = minify_js_file!("tests/test.js");
245/// assert_eq!(JS, "var a=(()=>{let a=`1`;a==1&&console.log(a)})");
246/// ```
247#[proc_macro]
248pub fn minify_js_file(input: TokenStream) -> TokenStream {
249    minify_js("minify_js_str", input, |filename| {
250        match fs::read_to_string(Path::new(filename)) {
251            Ok(s) => s,
252            Err(e) => panic!("minify_js_file error when reading file {}: {}", filename, &e),
253        }
254    })
255}
256
257// Minify a JS stream.
258fn minify_html(fct_name: &str, input: TokenStream, f: DataFunction) -> TokenStream {
259    let argument = parse_only_one_input(input, fct_name);
260
261    let data_to_minimize = f(&argument);
262
263    let mut html_minifier = HTMLMinifier::new();
264
265    html_minifier.digest(data_to_minimize).unwrap();
266
267    let out = html_minifier.get_html();
268
269    let new_data = String::from_utf8_lossy(&out).to_string();
270
271    let minified = format!( "r####\"{}\"####", new_data);
272    
273    TokenStream::from_str(&minified).unwrap()
274}
275
276/// Produce a minified html from string.
277///
278/// ```rust
279/// use static_web_minify::minify_html_str;
280///
281/// const HTML: &str = minify_html_str!(r#"
282///     <html>
283///         <head>
284///             <title>Test</title>
285///         </head>
286///         <body>
287///             <!-- Comment -->
288///             Hello
289///         </body>
290///     </html>
291/// "#);
292/// 
293/// assert_eq!(HTML, "<html>\n<head>\n<title>Test</title>\n</head>\n<body>\nHello\n</body>\n</html>");
294/// ```
295///
296/// ```rust
297/// use static_web_minify::minify_html_str;
298///
299/// const HTML: &str = minify_html_str!("
300///     <html>
301///         <head>
302///             <title>Test</title>
303///         </head>
304///         <body>
305///             <!-- Comment -->
306///             Hello
307///         </body>
308///     </html>
309/// ");
310/// 
311/// assert_eq!(HTML, "<html>\n<head>\n<title>Test</title>\n</head>\n<body>\nHello\n</body>\n</html>");
312/// ```
313#[proc_macro]
314pub fn minify_html_str(input: TokenStream) -> TokenStream {
315    minify_html("minify_html_str", input, |s| s.to_string())
316}
317
318/// Produce a minified html from file.
319///
320/// ```rust
321/// use static_web_minify::minify_html_file;
322///
323/// const HTML: &str = minify_html_file!("tests/test.html");
324/// assert_eq!(HTML, "<html>\n<head>\n<title>Test</title>\n</head>\n<body>\nHello\n</body>\n</html>");
325/// ```
326#[proc_macro]
327pub fn minify_html_file(input: TokenStream) -> TokenStream {
328    minify_html("minify_html_file", input, |filename| {
329        match fs::read_to_string(Path::new(filename)) {
330            Ok(s) => s,
331            Err(e) => panic!("minify_html_file error when reading file {}: {}", filename, &e),
332        }
333    })
334}
335
336// GZip  stream.
337fn gzip(fct_name: &str, input: TokenStream, f: DataFunction) -> TokenStream {
338    let (argument, varname) = parse_only_two_input(input, fct_name);
339
340    let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
341
342    let data_to_minimize = f(&argument);
343
344    if let Err(e) = e.write_all(data_to_minimize.as_bytes()) {
345        panic!("{} error to add string in Gzip system {}: {}", fct_name, &data_to_minimize, &e);
346    }
347
348    let compressed_bytes = e.finish();
349
350    if let Err(e) = compressed_bytes {
351        panic!("{} error to compress string {}: {}", fct_name, &data_to_minimize, &e)
352    }
353
354    let mut data_array: Vec<String> = vec![];
355
356    for b in compressed_bytes.unwrap() {
357        data_array.push(format!("0x{:02x?}", b));
358    }
359
360    let minified = format!("const {}: [u8; {}] = [{}];", varname, data_array.len(), data_array.join(", "));
361    
362    TokenStream::from_str(&minified).unwrap()
363}
364
365
366/// Produce a gzip stream from string.
367///
368/// ```rust
369/// use static_web_minify::gzip_str;
370///
371/// gzip_str!(HTML_GZIP, r#"
372///     <html>
373///         <head>
374///             <title>Test</title>
375///         </head>
376///         <body>
377///             <!-- Comment -->
378///             Hello
379///         </body>
380///     </html>
381/// "#);
382/// 
383/// assert_eq!(HTML_GZIP, [0x78, 0x9c, 0x5d, 0x4e, 0xcb, 0x0d, 0x80, 0x20, 0x0c, 0xbd, 0x33, 0x45, 0x1d, 0xa0, 0x61, 0x81, 0x86, 0x8b, 0x17, 0x07, 0x70, 0x01, 0x0d, 0x4d, 0x30, 0x29, 0x72, 0xb0, 0x17, 0xb7, 0x97, 0xa0, 0x49, 0x85, 0x77, 0x6a, 0xdf, 0x2f, 0xcf, 0x41, 0x05, 0x25, 0xcd, 0x12, 0x1c, 0x7c, 0xa0, 0xc4, 0x5b, 0xb4, 0xb7, 0x51, 0x7a, 0xa8, 0x70, 0x58, 0xf9, 0x52, 0xf2, 0xef, 0x6d, 0x76, 0xdf, 0xfb, 0x69, 0x2f, 0xf1, 0x1e, 0xe2, 0x13, 0x22, 0xcc, 0x25, 0x67, 0x3e, 0x15, 0x10, 0x7b, 0x71, 0x61, 0x91, 0xf2, 0x6b, 0xb3, 0x78, 0x6d, 0x6e, 0xc3, 0x1e, 0xe1, 0xe6, 0x24, 0x95]);
384/// ```
385///
386/// ```rust
387/// use static_web_minify::gzip_str;
388///
389/// gzip_str!(HTML_GZIP, "
390///     <html>
391///         <head>
392///             <title>Test</title>
393///         </head>
394///         <body>
395///             <!-- Comment -->
396///             Hello
397///         </body>
398///     </html>
399/// ");
400/// 
401/// assert_eq!(HTML_GZIP, [0x78, 0x9c, 0x5d, 0x4e, 0xcb, 0x0d, 0x80, 0x20, 0x0c, 0xbd, 0x33, 0x45, 0x1d, 0xa0, 0x61, 0x81, 0x86, 0x8b, 0x17, 0x07, 0x70, 0x01, 0x0d, 0x4d, 0x30, 0x29, 0x72, 0xb0, 0x17, 0xb7, 0x97, 0xa0, 0x49, 0x85, 0x77, 0x6a, 0xdf, 0x2f, 0xcf, 0x41, 0x05, 0x25, 0xcd, 0x12, 0x1c, 0x7c, 0xa0, 0xc4, 0x5b, 0xb4, 0xb7, 0x51, 0x7a, 0xa8, 0x70, 0x58, 0xf9, 0x52, 0xf2, 0xef, 0x6d, 0x76, 0xdf, 0xfb, 0x69, 0x2f, 0xf1, 0x1e, 0xe2, 0x13, 0x22, 0xcc, 0x25, 0x67, 0x3e, 0x15, 0x10, 0x7b, 0x71, 0x61, 0x91, 0xf2, 0x6b, 0xb3, 0x78, 0x6d, 0x6e, 0xc3, 0x1e, 0xe1, 0xe6, 0x24, 0x95]);
402/// ```
403#[proc_macro]
404pub fn gzip_str(input: TokenStream) -> TokenStream {
405    gzip("gzip_str", input, |s| s.to_string())
406} 
407/// Produce a gzip stream from file.
408///
409/// ```rust
410/// use static_web_minify::gzip_file;
411///
412/// gzip_file!(HTML_GZIP, "tests/test.html");
413/// 
414/// assert_eq!(HTML_GZIP, [0x78, 0x9c, 0x4d, 0x8d, 0xb1, 0x0d, 0x80, 0x30, 0x0c, 0x04, 0x7b, 0xa6, 0x30, 0x03, 0x58, 0x59, 0xc0, 0x4a, 0x43, 0xc3, 0x00, 0x2c, 0x00, 0x8a, 0xa5, 0x20, 0xd9, 0xb8, 0xc0, 0x0d, 0xdb, 0x13, 0x30, 0x52, 0x70, 0xf5, 0xd6, 0xff, 0xfd, 0x53, 0x75, 0x95, 0x3c, 0x40, 0x3b, 0xaa, 0xbc, 0x96, 0x90, 0xef, 0xeb, 0xbb, 0x0b, 0xe7, 0x85, 0x4f, 0xa7, 0x14, 0x3a, 0x62, 0xa9, 0xe7, 0x68, 0xb3, 0x72, 0xfd, 0x90, 0x11, 0x11, 0x26, 0x53, 0xe5, 0xc3, 0x01, 0xb1, 0x1b, 0x33, 0x8b, 0xd8, 0x47, 0x07, 0xd2, 0x5a, 0x9e, 0xe1, 0x1b, 0x13, 0xaf, 0x20, 0x01]);
415/// ```
416#[proc_macro]
417pub fn gzip_file(input: TokenStream) -> TokenStream {
418    gzip("gzip_file", input, |filename| {
419        match fs::read_to_string(Path::new(filename)) {
420            Ok(s) => s,
421            Err(e) => panic!("gzip_file error when reading file {}: {}", filename, &e),
422        }
423    })
424}