163 lines
5.4 KiB
Rust
163 lines
5.4 KiB
Rust
use std::fs::File;
|
|
use std::io::{stdin, stdout, Read, Write};
|
|
use std::process::exit;
|
|
use std::sync::Arc;
|
|
|
|
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
|
use structopt::StructOpt;
|
|
|
|
use minify_html::{minify, Cfg};
|
|
|
|
#[derive(StructOpt)]
|
|
#[structopt(
|
|
name = "minify-html",
|
|
about = "Extremely fast and smart HTML + JS + CSS minifier"
|
|
)]
|
|
// WARNING: Keep descriptions in sync with Cfg.
|
|
struct Cli {
|
|
/// Files to minify; omit for stdin. If more than one is provided, they will be parallel minified in place, and --output must be omitted.
|
|
#[structopt(parse(from_os_str))]
|
|
inputs: Vec<std::path::PathBuf>,
|
|
|
|
/// Output destination; omit for stdout.
|
|
#[structopt(short, long, parse(from_os_str))]
|
|
output: Option<std::path::PathBuf>,
|
|
|
|
/// Minify JS in `<script>` tags that have a valid or no `type` attribute value.
|
|
#[structopt(long)]
|
|
minify_js: bool,
|
|
|
|
/// Minify CSS in `<style>` tags and `style` attributes.
|
|
#[structopt(long)]
|
|
minify_css: bool,
|
|
|
|
#[structopt(long)]
|
|
/// Do not minify DOCTYPEs. Minified DOCTYPEs may not be spec compliant.
|
|
do_not_minify_doctype: bool,
|
|
|
|
/// Ensure all unquoted attribute values in the output do not contain any characters prohibited by the WHATWG specification.
|
|
#[structopt(long)]
|
|
ensure_spec_compliant_unquoted_attribute_values: bool,
|
|
|
|
/// Do not omit closing tags when possible.
|
|
#[structopt(long)]
|
|
keep_closing_tags: bool,
|
|
|
|
/// Do not omit `<html>` and `<head>` opening tags when they don't have attributes.
|
|
#[structopt(long)]
|
|
keep_html_and_head_opening_tags: bool,
|
|
|
|
/// Keep spaces between attributes when possible to conform to HTML standards.
|
|
#[structopt(long)]
|
|
keep_spaces_between_attributes: bool,
|
|
|
|
/// Keep all comments.
|
|
#[structopt(long)]
|
|
keep_comments: bool,
|
|
|
|
/// Remove all bangs.
|
|
#[structopt(long)]
|
|
remove_bangs: bool,
|
|
|
|
/// Remove all processing_instructions.
|
|
#[structopt(long)]
|
|
remove_processing_instructions: bool,
|
|
}
|
|
|
|
macro_rules! io_expect {
|
|
($name:expr, $expr:expr, $msg:literal) => {
|
|
match $expr {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
eprintln!("[{}] {}: {}", $name, $msg, e);
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
fn main() {
|
|
let args = Cli::from_args();
|
|
if args.output.is_some() && args.inputs.len() > 1 {
|
|
eprintln!("Cannot provide --output when multiple inputs are provided.");
|
|
exit(1);
|
|
};
|
|
|
|
let cfg = Arc::new(Cfg {
|
|
do_not_minify_doctype: args.do_not_minify_doctype,
|
|
ensure_spec_compliant_unquoted_attribute_values: args
|
|
.ensure_spec_compliant_unquoted_attribute_values,
|
|
keep_closing_tags: args.keep_closing_tags,
|
|
keep_comments: args.keep_comments,
|
|
keep_html_and_head_opening_tags: args.keep_html_and_head_opening_tags,
|
|
keep_spaces_between_attributes: args.keep_spaces_between_attributes,
|
|
minify_css: args.minify_css,
|
|
minify_js: args.minify_js,
|
|
remove_bangs: args.remove_bangs,
|
|
remove_processing_instructions: args.remove_processing_instructions,
|
|
});
|
|
|
|
if args.inputs.len() <= 1 {
|
|
// Single file mode or stdin mode.
|
|
let input_name = args
|
|
.inputs
|
|
.get(0)
|
|
.map(|p| p.to_string_lossy().into_owned())
|
|
.unwrap_or_else(|| "stdin".to_string());
|
|
let mut src_file: Box<dyn Read> = match args.inputs.get(0) {
|
|
Some(p) => Box::new(io_expect!(
|
|
input_name,
|
|
File::open(p),
|
|
"Could not open source file"
|
|
)),
|
|
None => Box::new(stdin()),
|
|
};
|
|
let mut src_code = Vec::<u8>::new();
|
|
io_expect!(
|
|
input_name,
|
|
src_file.read_to_end(&mut src_code),
|
|
"Could not load source code"
|
|
);
|
|
let out_code = minify(&src_code, &cfg);
|
|
let mut out_file: Box<dyn Write> = match args.output {
|
|
Some(p) => Box::new(io_expect!(
|
|
input_name,
|
|
File::create(p),
|
|
"Could not open output file"
|
|
)),
|
|
None => Box::new(stdout()),
|
|
};
|
|
io_expect!(
|
|
input_name,
|
|
out_file.write_all(&out_code),
|
|
"Could not save minified code"
|
|
);
|
|
} else {
|
|
args.inputs.par_iter().for_each(|input| {
|
|
let input_name = input.to_string_lossy().into_owned();
|
|
|
|
let mut src_file =
|
|
io_expect!(input_name, File::open(&input), "Could not open source file");
|
|
let mut src_code = Vec::<u8>::new();
|
|
io_expect!(
|
|
input_name,
|
|
src_file.read_to_end(&mut src_code),
|
|
"Could not load source code"
|
|
);
|
|
let out_code = minify(&src_code, &cfg);
|
|
let mut out_file = io_expect!(
|
|
input_name,
|
|
File::create(&input),
|
|
"Could not open output file"
|
|
);
|
|
io_expect!(
|
|
input_name,
|
|
out_file.write_all(&out_code),
|
|
"Could not save minified code"
|
|
);
|
|
// Just print the name, since this is the default output and any prefix becomes redundant. It'd also allow piping into another command (quite nice for something like `minify-html *.html | xargs gzip`), copying as list of files, etc.
|
|
println!("{}", input_name);
|
|
});
|
|
}
|
|
}
|