//! Include a minified css, js, html file as an inline const in your high-performance compiled web
//! application. Inspired by [const-css-minify](https://github.com/scpso/const-css-minify)
//!
//! Unlinke `include_str!()`, you must put the name file from root of your rust project (see
//! [issue 54725](https://github.com/rust-lang/rust/issues/54725)).
//!
//! ```rust
//! use static_web_minify::minify_js_file;
//!
//! // this is probably the pattern you want to use
//! const JS: &str = minify_js_file!("tests/test.js");
//! ```
//!
//! It's also possible to include a raw string with:
//! ```rust
//! use static_web_minify::minify_js_str;
//!
//! const JS: &str = minify_js_str!(r#"
//! function my_func() {
//! // Great !
//! }
//! "#);
//! assert_eq!(JS, "var my_func=(()=>{})");
//! ```
//!
//! 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).
use proc_macro::TokenStream;
use std::str::FromStr;
use css_minify::optimizations::{Minifier, Level};
use std::fs;
use std::path::Path;
use minify_js::{Session, TopLevelMode, minify};
use html_minifier::HTMLMinifier;
use std::io::prelude::*;
use flate2::Compression;
use flate2::write::ZlibEncoder;
/// Type to tranforme data sting or file name in data
type DataFunction = fn(&str) -> String;
/// Control that only one input is give to macro
fn parse_only_one_input(input: TokenStream, fct_name: &str) -> String {
let tokens: Vec<_> = input.into_iter().collect();
if tokens.len() != 1 {
panic!("{} requires only a single str as input!", fct_name);
}
match tokens.first() {
Some(f) => parse_filemane_or_code_input(f.to_string()),
None => {
panic!("{} requires only a single str as input!", fct_name);
}
}
}
/// Control that only one input is give to macro
fn parse_only_two_input(input: TokenStream, fct_name: &str) -> (String, String) {
let tokens: Vec<_> = input.into_iter().collect();
if tokens.len() != 3 { // 3 -> we have a comma
panic!("{} requires two arguments. First is str as input!", fct_name);
}
let varname = match tokens.first() {
Some(f) => match parse_varname_input(f.to_string()) {
Some(f) => f,
None => {
panic!("{} first argument requiere a const/static name", fct_name)
}
},
None => {
panic!("{} requires two arguments. First is str as input!", fct_name)
}
};
let comma = match tokens.get(1) {
Some(f) => f.to_string(),
None => ",".to_string()
};
if comma != "," {
panic!("{} requires two arguments, separated by comma!", fct_name);
}
let filename = match tokens.last() {
Some(f) => parse_filemane_or_code_input(f.to_string()),
None => {
panic!("{} requires two arguments. First is str as input!", fct_name)
}
};
(filename, varname)
}
/// Extra string
fn parse_filemane_or_code_input(mut first_token: String) -> String {
let data_len = first_token.len();
// Now check if start by is " or r#"
if first_token.starts_with("r#\"") {
first_token = first_token[3..data_len - 2].to_string();
} else {
first_token = first_token[1..data_len - 1].to_string();
}
first_token
}
/// Extract var name
fn parse_varname_input(token: String) -> Option<String> {
for c in token.chars() {
match c {
'a'..='z' => {},
'A'..='Z' => {},
'0'..='9' => {},
'_' => {},
_ => return None
}
}
Some(token)
}
// Minify a CSS stream.
fn minify_css(fct_name: &str, input: TokenStream, f: DataFunction) -> TokenStream {
let argument = parse_only_one_input(input, fct_name);
let data_to_minimize = f(&argument);
let result = Minifier::default().minify(&data_to_minimize, Level::One);
if let Err(e) = result {
panic!("{} has an issue to minimify CSS string: '{}', {}", fct_name, e, data_to_minimize);
}
let minified = format!( "r####\"{}\"####", result.unwrap());
TokenStream::from_str(&minified).unwrap()
}
/// Produce a minified css from string.
///
/// ```rust
/// use static_web_minify::minify_css_str;
///
/// const CSS: &str = minify_css_str!(r#"
/// body {
/// color: #fff;
/// }
/// "#);
///
/// assert_eq!(CSS, "body{color:#fff}");
/// ```
///
/// ```rust
/// use static_web_minify::minify_css_str;
///
/// const CSS: &str = minify_css_str!("
/// body {
/// color: #fff;
/// }
/// ");
///
/// assert_eq!(CSS, "body{color:#fff}");
/// ```
#[proc_macro]
pub fn minify_css_str(input: TokenStream) -> TokenStream {
minify_css("minify_css_str", input, |s| s.to_string())
}
/// Produce a minified css from file.
///
/// ```rust
/// use static_web_minify::minify_css_file;
///
/// const CSS: &str = minify_css_file!("tests/test.css");
/// assert_eq!(CSS, "body{color:#fff}");
/// ```
#[proc_macro]
pub fn minify_css_file(input: TokenStream) -> TokenStream {
minify_css("minify_css_file", input, |filename|{
match fs::read_to_string(Path::new(filename)) {
Ok(s) => s,
Err(e) => panic!("minify_css_file error when reading file {}: {}", filename, &e),
}
})
}
// Minify a JS stream.
fn minify_js(fct_name: &str, input: TokenStream, f: DataFunction) -> TokenStream {
let argument = parse_only_one_input(input, fct_name);
let data_to_minimize = f(&argument);
let code = data_to_minimize.as_bytes();
let session = Session::new();
let mut out = Vec::new();
if let Err(e) = minify(&session, TopLevelMode::Global, code, &mut out) {
panic!("{} has an issue to minimify string: '{}', {}", fct_name, e, data_to_minimize);
}
let minified = format!( "r####\"{}\"####", String::from_utf8_lossy(&out));
TokenStream::from_str(&minified).unwrap()
}
/// Produce a minified js from string.
///
/// ```rust
/// use static_web_minify::minify_js_str;
///
/// const JS: &str = minify_js_str!(r#"
/// function my_func() {
/// // Great !
/// }
/// "#);
///
/// assert_eq!(JS, "var my_func=(()=>{})");
/// ```
///
/// ```rust
/// use static_web_minify::minify_js_str;
///
/// const JS: &str = minify_js_str!("
/// function my_func() {
/// // Great !
/// }
/// ");
///
/// assert_eq!(JS, "var my_func=(()=>{})");
/// ```
#[proc_macro]
pub fn minify_js_str(input: TokenStream) -> TokenStream {
minify_js("minify_js_str", input, |s| s.to_string())
}
/// Produce a minified js from file.
///
/// ```rust
/// use static_web_minify::minify_js_file;
///
/// const JS: &str = minify_js_file!("tests/test.js");
/// assert_eq!(JS, "var a=(()=>{let a=`1`;a==1&&console.log(a)})");
/// ```
#[proc_macro]
pub fn minify_js_file(input: TokenStream) -> TokenStream {
minify_js("minify_js_str", input, |filename| {
match fs::read_to_string(Path::new(filename)) {
Ok(s) => s,
Err(e) => panic!("minify_js_file error when reading file {}: {}", filename, &e),
}
})
}
// Minify a JS stream.
fn minify_html(fct_name: &str, input: TokenStream, f: DataFunction) -> TokenStream {
let argument = parse_only_one_input(input, fct_name);
let data_to_minimize = f(&argument);
let mut html_minifier = HTMLMinifier::new();
html_minifier.digest(data_to_minimize).unwrap();
let out = html_minifier.get_html();
let new_data = String::from_utf8_lossy(&out).to_string();
let minified = format!( "r####\"{}\"####", new_data);
TokenStream::from_str(&minified).unwrap()
}
/// Produce a minified html from string.
///
/// ```rust
/// use static_web_minify::minify_html_str;
///
/// const HTML: &str = minify_html_str!(r#"
/// <html>
/// <head>
/// <title>Test</title>
/// </head>
/// <body>
/// <!-- Comment -->
/// Hello
/// </body>
/// </html>
/// "#);
///
/// assert_eq!(HTML, "<html>\n<head>\n<title>Test</title>\n</head>\n<body>\nHello\n</body>\n</html>");
/// ```
///
/// ```rust
/// use static_web_minify::minify_html_str;
///
/// const HTML: &str = minify_html_str!("
/// <html>
/// <head>
/// <title>Test</title>
/// </head>
/// <body>
/// <!-- Comment -->
/// Hello
/// </body>
/// </html>
/// ");
///
/// assert_eq!(HTML, "<html>\n<head>\n<title>Test</title>\n</head>\n<body>\nHello\n</body>\n</html>");
/// ```
#[proc_macro]
pub fn minify_html_str(input: TokenStream) -> TokenStream {
minify_html("minify_html_str", input, |s| s.to_string())
}
/// Produce a minified html from file.
///
/// ```rust
/// use static_web_minify::minify_html_file;
///
/// const HTML: &str = minify_html_file!("tests/test.html");
/// assert_eq!(HTML, "<html>\n<head>\n<title>Test</title>\n</head>\n<body>\nHello\n</body>\n</html>");
/// ```
#[proc_macro]
pub fn minify_html_file(input: TokenStream) -> TokenStream {
minify_html("minify_html_file", input, |filename| {
match fs::read_to_string(Path::new(filename)) {
Ok(s) => s,
Err(e) => panic!("minify_html_file error when reading file {}: {}", filename, &e),
}
})
}
// GZip stream.
fn gzip(fct_name: &str, input: TokenStream, f: DataFunction) -> TokenStream {
let (argument, varname) = parse_only_two_input(input, fct_name);
let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
let data_to_minimize = f(&argument);
if let Err(e) = e.write_all(data_to_minimize.as_bytes()) {
panic!("{} error to add string in Gzip system {}: {}", fct_name, &data_to_minimize, &e);
}
let compressed_bytes = e.finish();
if let Err(e) = compressed_bytes {
panic!("{} error to compress string {}: {}", fct_name, &data_to_minimize, &e)
}
let mut data_array: Vec<String> = vec![];
for b in compressed_bytes.unwrap() {
data_array.push(format!("0x{:02x?}", b));
}
let minified = format!("const {}: [u8; {}] = [{}];", varname, data_array.len(), data_array.join(", "));
TokenStream::from_str(&minified).unwrap()
}
/// Produce a gzip stream from string.
///
/// ```rust
/// use static_web_minify::gzip_str;
///
/// gzip_str!(HTML_GZIP, r#"
/// <html>
/// <head>
/// <title>Test</title>
/// </head>
/// <body>
/// <!-- Comment -->
/// Hello
/// </body>
/// </html>
/// "#);
///
/// 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]);
/// ```
///
/// ```rust
/// use static_web_minify::gzip_str;
///
/// gzip_str!(HTML_GZIP, "
/// <html>
/// <head>
/// <title>Test</title>
/// </head>
/// <body>
/// <!-- Comment -->
/// Hello
/// </body>
/// </html>
/// ");
///
/// 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]);
/// ```
#[proc_macro]
pub fn gzip_str(input: TokenStream) -> TokenStream {
gzip("gzip_str", input, |s| s.to_string())
}
/// Produce a gzip stream from file.
///
/// ```rust
/// use static_web_minify::gzip_file;
///
/// gzip_file!(HTML_GZIP, "tests/test.html");
///
/// 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]);
/// ```
#[proc_macro]
pub fn gzip_file(input: TokenStream) -> TokenStream {
gzip("gzip_file", input, |filename| {
match fs::read_to_string(Path::new(filename)) {
Ok(s) => s,
Err(e) => panic!("gzip_file error when reading file {}: {}", filename, &e),
}
})
}