CmdLineCompressor.java
/*
* Copyright 2009-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.googlecode.htmlcompressor;
import com.google.javascript.jscomp.CompilationLevel;
import com.google.javascript.jscomp.SourceFile;
import com.googlecode.htmlcompressor.analyzer.HtmlAnalyzer;
import com.googlecode.htmlcompressor.compressor.ClosureJavaScriptCompressor;
import com.googlecode.htmlcompressor.compressor.Compressor;
import com.googlecode.htmlcompressor.compressor.HtmlCompressor;
import com.googlecode.htmlcompressor.compressor.XmlCompressor;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jargs.gnu.CmdLineParser;
import jargs.gnu.CmdLineParser.Option;
import jargs.gnu.CmdLineParser.OptionException;
/**
* Wrapper for HTML and XML compressor classes that allows using them from a command line.
* <p>
* Usage: <code>java -jar htmlcompressor.jar [options] [input]</code>
* <p>
* To view a list of all available parameters please run with <code>-?</code> option:
* <p>
* <code>java -jar htmlcompressor.jar -?</code>
*
* @author <a href="mailto:serg472@gmail.com">Sergiy Kovalchuk</a>
*/
public class CmdLineCompressor {
/** The Constant logger. */
private static final Logger logger = LoggerFactory.getLogger(CmdLineCompressor.class);
/** The Constant urlPattern. */
private static final Pattern urlPattern = Pattern.compile("^https?://.*$", Pattern.CASE_INSENSITIVE);
/** The help opt. */
private boolean helpOpt;
/** The analyze opt. */
private boolean analyzeOpt;
/** The charset opt. */
private Charset charsetOpt;
/** The output filename opt. */
private String outputFilenameOpt;
/** The patterns filename opt. */
private String patternsFilenameOpt;
/** The type opt. */
private String typeOpt;
/** The filemask opt. */
private String filemaskOpt;
/** The recursive opt. */
private boolean recursiveOpt;
/** The preserve comments opt. */
private boolean preserveCommentsOpt;
/** The preserve intertag spaces opt. */
private boolean preserveIntertagSpacesOpt;
/** The preserve multi spaces opt. */
private boolean preserveMultiSpacesOpt;
/** The remove intertag spaces opt. */
private boolean removeIntertagSpacesOpt;
/** The remove quotes opt. */
private boolean removeQuotesOpt;
/** The remove surrounding spaces opt. */
private String removeSurroundingSpacesOpt;
/** The preserve line breaks opt. */
private boolean preserveLineBreaksOpt;
/** The preserve php tags opt. */
private boolean preservePhpTagsOpt;
/** The preserve server script tags opt. */
private boolean preserveServerScriptTagsOpt;
/** The preserve ssi tags opt. */
private boolean preserveSsiTagsOpt;
/** The compress js opt. */
private boolean compressJsOpt;
/** The compress css opt. */
private boolean compressCssOpt;
/** The js compressor opt. */
private String jsCompressorOpt;
/** The simple doctype opt. */
private boolean simpleDoctypeOpt;
/** The remove script attributes opt. */
private boolean removeScriptAttributesOpt;
/** The remove style attributes opt. */
private boolean removeStyleAttributesOpt;
/** The remove link attributes opt. */
private boolean removeLinkAttributesOpt;
/** The remove form attributes opt. */
private boolean removeFormAttributesOpt;
/** The remove input attributes opt. */
private boolean removeInputAttributesOpt;
/** The simple boolean attributes opt. */
private boolean simpleBooleanAttributesOpt;
/** The remove java script protocol opt. */
private boolean removeJavaScriptProtocolOpt;
/** The remove http protocol opt. */
private boolean removeHttpProtocolOpt;
/** The remove https protocol opt. */
private boolean removeHttpsProtocolOpt;
/** The nomunge opt. */
private boolean nomungeOpt;
/** The linebreak opt. */
private int linebreakOpt;
/** The preserve semi opt. */
private boolean preserveSemiOpt;
/** The disable optimizations opt. */
private boolean disableOptimizationsOpt;
/** The closure opt level opt. */
private String closureOptLevelOpt;
/** The closure custom externs only opt. */
private boolean closureCustomExternsOnlyOpt;
/** The closure externs opt. */
private List<String> closureExternsOpt;
/** The file args opt. */
private List<String> fileArgsOpt;
/**
* Instantiates a new cmd line compressor.
*
* @param args
* the args
*/
public CmdLineCompressor(String[] args) {
CmdLineParser parser = new CmdLineParser();
Option helpOption = parser.addBooleanOption('h', "help");
Option helpOptAlt = parser.addBooleanOption('?', "help_alt");
Option analyzeOption = parser.addBooleanOption('a', "analyze");
Option recursiveOption = parser.addBooleanOption('r', "recursive");
Option charsetOption = parser.addStringOption('c', "charset");
Option outputFilenameOption = parser.addStringOption('o', "output");
Option patternsFilenameOption = parser.addStringOption('p', "preserve");
Option typeOption = parser.addStringOption('t', "type");
Option filemaskOption = parser.addStringOption('m', "mask");
Option preserveCommentsOption = parser.addBooleanOption("preserve-comments");
Option preserveIntertagSpacesOption = parser.addBooleanOption("preserve-intertag-spaces");
Option preserveMultiSpacesOption = parser.addBooleanOption("preserve-multi-spaces");
Option removeIntertagSpacesOption = parser.addBooleanOption("remove-intertag-spaces");
Option removeSurroundingSpacesOption = parser.addStringOption("remove-surrounding-spaces");
Option removeQuotesOption = parser.addBooleanOption("remove-quotes");
Option preserveLineBreaksOption = parser.addBooleanOption("preserve-line-breaks");
Option preservePhpTagsOption = parser.addBooleanOption("preserve-php");
Option preserveServerScriptTagsOption = parser.addBooleanOption("preserve-server-script");
Option preserveSsiTagsOption = parser.addBooleanOption("preserve-ssi");
Option compressJsOption = parser.addBooleanOption("compress-js");
Option compressCssOption = parser.addBooleanOption("compress-css");
Option jsCompressorOption = parser.addStringOption("js-compressor");
Option simpleDoctypeOption = parser.addBooleanOption("simple-doctype");
Option removeScriptAttributesOption = parser.addBooleanOption("remove-script-attr");
Option removeStyleAttributesOption = parser.addBooleanOption("remove-style-attr");
Option removeLinkAttributesOption = parser.addBooleanOption("remove-link-attr");
Option removeFormAttributesOption = parser.addBooleanOption("remove-form-attr");
Option removeInputAttributesOption = parser.addBooleanOption("remove-input-attr");
Option simpleBooleanAttributesOption = parser.addBooleanOption("simple-bool-attr");
Option removeJavaScriptProtocolOption = parser.addBooleanOption("remove-js-protocol");
Option removeHttpProtocolOption = parser.addBooleanOption("remove-http-protocol");
Option removeHttpsProtocolOption = parser.addBooleanOption("remove-https-protocol");
Option nomungeOption = parser.addBooleanOption("nomunge");
Option linebreakOption = parser.addStringOption("line-break");
Option preserveSemiOption = parser.addBooleanOption("preserve-semi");
Option disableOptimizationsOption = parser.addBooleanOption("disable-optimizations");
Option closureOptLevelOption = parser.addStringOption("closure-opt-level");
Option closureCustomExternsOnlyOption = parser.addBooleanOption("closure-custom-externs-only");
Option closureExternsOption = parser.addStringOption("closure-externs");
try {
parser.parse(args);
this.helpOpt = (Boolean) parser.getOptionValue(helpOption, false)
|| (Boolean) parser.getOptionValue(helpOptAlt, false);
this.analyzeOpt = (Boolean) parser.getOptionValue(analyzeOption, false);
this.recursiveOpt = (Boolean) parser.getOptionValue(recursiveOption, false);
this.charsetOpt = Charset.forName((String) parser.getOptionValue(charsetOption, "UTF-8"));
this.outputFilenameOpt = (String) parser.getOptionValue(outputFilenameOption);
this.patternsFilenameOpt = (String) parser.getOptionValue(patternsFilenameOption);
this.typeOpt = (String) parser.getOptionValue(typeOption);
this.filemaskOpt = (String) parser.getOptionValue(filemaskOption);
this.preserveCommentsOpt = (Boolean) parser.getOptionValue(preserveCommentsOption, false);
this.preserveIntertagSpacesOpt = (Boolean) parser.getOptionValue(preserveIntertagSpacesOption, false);
this.preserveMultiSpacesOpt = (Boolean) parser.getOptionValue(preserveMultiSpacesOption, false);
this.removeIntertagSpacesOpt = (Boolean) parser.getOptionValue(removeIntertagSpacesOption, false);
this.removeQuotesOpt = (Boolean) parser.getOptionValue(removeQuotesOption, false);
this.preserveLineBreaksOpt = (Boolean) parser.getOptionValue(preserveLineBreaksOption, false);
this.preservePhpTagsOpt = (Boolean) parser.getOptionValue(preservePhpTagsOption, false);
this.preserveServerScriptTagsOpt = (Boolean) parser.getOptionValue(preserveServerScriptTagsOption, false);
this.preserveSsiTagsOpt = (Boolean) parser.getOptionValue(preserveSsiTagsOption, false);
this.compressJsOpt = (Boolean) parser.getOptionValue(compressJsOption, false);
this.compressCssOpt = (Boolean) parser.getOptionValue(compressCssOption, false);
this.jsCompressorOpt = (String) parser.getOptionValue(jsCompressorOption, HtmlCompressor.JS_COMPRESSOR_YUI);
this.simpleDoctypeOpt = (Boolean) parser.getOptionValue(simpleDoctypeOption, false);
this.removeScriptAttributesOpt = (Boolean) parser.getOptionValue(removeScriptAttributesOption, false);
this.removeStyleAttributesOpt = (Boolean) parser.getOptionValue(removeStyleAttributesOption, false);
this.removeLinkAttributesOpt = (Boolean) parser.getOptionValue(removeLinkAttributesOption, false);
this.removeFormAttributesOpt = (Boolean) parser.getOptionValue(removeFormAttributesOption, false);
this.removeInputAttributesOpt = (Boolean) parser.getOptionValue(removeInputAttributesOption, false);
this.simpleBooleanAttributesOpt = (Boolean) parser.getOptionValue(simpleBooleanAttributesOption, false);
this.removeJavaScriptProtocolOpt = (Boolean) parser.getOptionValue(removeJavaScriptProtocolOption, false);
this.removeHttpProtocolOpt = (Boolean) parser.getOptionValue(removeHttpProtocolOption, false);
this.removeHttpsProtocolOpt = (Boolean) parser.getOptionValue(removeHttpsProtocolOption, false);
this.nomungeOpt = (Boolean) parser.getOptionValue(nomungeOption, false);
this.linebreakOpt = (Integer) parser.getOptionValue(linebreakOption, -1);
this.preserveSemiOpt = (Boolean) parser.getOptionValue(preserveSemiOption, false);
this.disableOptimizationsOpt = (Boolean) parser.getOptionValue(disableOptimizationsOption, false);
this.closureOptLevelOpt = (String) parser.getOptionValue(closureOptLevelOption,
ClosureJavaScriptCompressor.COMPILATION_LEVEL_SIMPLE);
this.closureCustomExternsOnlyOpt = (Boolean) parser.getOptionValue(closureCustomExternsOnlyOption, false);
this.closureExternsOpt = parser.getOptionValues(closureExternsOption);
this.removeSurroundingSpacesOpt = (String) parser.getOptionValue(removeSurroundingSpacesOption);
if (this.removeSurroundingSpacesOpt != null) {
if ("min".equalsIgnoreCase(this.removeSurroundingSpacesOpt)) {
this.removeSurroundingSpacesOpt = HtmlCompressor.BLOCK_TAGS_MIN;
} else if ("max".equalsIgnoreCase(this.removeSurroundingSpacesOpt)) {
this.removeSurroundingSpacesOpt = HtmlCompressor.BLOCK_TAGS_MAX;
} else if ("all".equalsIgnoreCase(this.removeSurroundingSpacesOpt)) {
this.removeSurroundingSpacesOpt = HtmlCompressor.ALL_TAGS;
}
}
// input file
this.fileArgsOpt = parser.getRemainingArgs();
// charset
this.charsetOpt = Charset.isSupported(this.charsetOpt.name()) ? this.charsetOpt : StandardCharsets.UTF_8;
// look for "/?"
for (int i = 0; i < args.length; i++) {
if ("/?".equals(args[i])) {
this.helpOpt = true;
break;
}
}
} catch (OptionException e) {
logger.info("{}", e.getMessage());
logger.trace("", e);
printUsage();
}
}
/**
* The main method.
*
* @param args
* the arguments
*/
public static void main(String[] args) {
CmdLineCompressor cmdLineCompressor = new CmdLineCompressor(args);
cmdLineCompressor.process();
}
/**
* Process.
*/
public void process() {
try {
// help
if (helpOpt) {
printUsage();
return;
}
// type
String type = typeOpt;
if (type != null && !"html".equalsIgnoreCase(type) && !"xml".equalsIgnoreCase(type)) {
throw new IllegalArgumentException("Unknown type: " + type);
}
if (fileArgsOpt.isEmpty()) {
// html by default for stdin
if (type == null) {
type = "html";
}
} else if (type == null) {
// detect type from extension
if (fileArgsOpt.get(0).toLowerCase(Locale.ENGLISH).endsWith(".xml")) {
type = "xml";
} else {
type = "html";
}
}
if (analyzeOpt) {
// analyzer mode
HtmlAnalyzer analyzer = new HtmlAnalyzer(
HtmlCompressor.JS_COMPRESSOR_CLOSURE.equalsIgnoreCase(jsCompressorOpt)
? HtmlCompressor.JS_COMPRESSOR_CLOSURE
: HtmlCompressor.JS_COMPRESSOR_YUI);
analyzer.analyze(readResource(buildReader(fileArgsOpt.isEmpty() ? null : fileArgsOpt.get(0))));
} else {
// compression mode
Compressor compressor = "xml".equalsIgnoreCase(type) ? createXmlCompressor() : createHtmlCompressor();
Map<String, String> ioMap = buildInputOutputMap();
for (Map.Entry<String, String> entry : ioMap.entrySet()) {
writeResource(compressor.compress(readResource(buildReader(entry.getKey()))),
buildWriter(entry.getValue()));
}
}
} catch (NoClassDefFoundError e) {
if (HtmlCompressor.JS_COMPRESSOR_CLOSURE.equalsIgnoreCase(jsCompressorOpt)) {
logger.info("""
ERROR: For JavaScript compression using Google Closure Compiler
additional jar file called compiler.jar must be present
in the same directory as HtmlCompressor jar
""");
} else {
logger.info("""
ERROR: For CSS or JavaScript compression using YUICompressor additional jar file
called yuicompressor.jar must be present\n" + "in the same directory as HtmlCompressor jar
""");
}
logger.trace("", e);
} catch (OptionException e) {
logger.info("{}", e.getMessage());
logger.trace("", e);
printUsage();
} catch (IOException | IllegalArgumentException e) {
logger.info("{}", e.getMessage());
logger.trace("", e);
}
}
/**
* Creates the html compressor.
*
* @return the compressor
*
* @throws OptionException
* the option exception
*/
private Compressor createHtmlCompressor() throws OptionException {
boolean useClosureCompressor = HtmlCompressor.JS_COMPRESSOR_CLOSURE.equalsIgnoreCase(jsCompressorOpt);
// custom preserve patterns
List<Pattern> preservePatterns = new ArrayList<>();
// predefined
if (preservePhpTagsOpt) {
preservePatterns.add(HtmlCompressor.PHP_TAG_PATTERN);
}
if (preserveServerScriptTagsOpt) {
preservePatterns.add(HtmlCompressor.SERVER_SCRIPT_TAG_PATTERN);
}
if (preserveSsiTagsOpt) {
preservePatterns.add(HtmlCompressor.SERVER_SIDE_INCLUDE_PATTERN);
}
if (patternsFilenameOpt != null) {
try (BufferedReader patternsIn = Files.newBufferedReader(Path.of(patternsFilenameOpt), charsetOpt)) {
String line = null;
while ((line = patternsIn.readLine()) != null) {
if (line.length() > 0) {
try {
preservePatterns.add(Pattern.compile(line));
} catch (PatternSyntaxException e) {
logger.trace("", e);
throw new IllegalArgumentException(
"Regular expression compilation error: " + e.getMessage());
}
}
}
} catch (IOException e) {
logger.trace("", e);
throw new IllegalArgumentException("Unable to read custom pattern definitions file: " + e.getMessage());
}
}
// set compressor options
HtmlCompressor htmlCompressor = new HtmlCompressor();
htmlCompressor.setRemoveComments(!preserveCommentsOpt);
htmlCompressor.setRemoveMultiSpaces(!preserveMultiSpacesOpt);
htmlCompressor.setRemoveIntertagSpaces(removeIntertagSpacesOpt);
htmlCompressor.setRemoveQuotes(removeQuotesOpt);
htmlCompressor.setPreserveLineBreaks(preserveLineBreaksOpt);
htmlCompressor.setCompressJavaScript(compressJsOpt);
htmlCompressor.setCompressCss(compressCssOpt);
htmlCompressor.setSimpleDoctype(simpleDoctypeOpt);
htmlCompressor.setRemoveScriptAttributes(removeScriptAttributesOpt);
htmlCompressor.setRemoveStyleAttributes(removeStyleAttributesOpt);
htmlCompressor.setRemoveLinkAttributes(removeLinkAttributesOpt);
htmlCompressor.setRemoveFormAttributes(removeFormAttributesOpt);
htmlCompressor.setRemoveInputAttributes(removeInputAttributesOpt);
htmlCompressor.setSimpleBooleanAttributes(simpleBooleanAttributesOpt);
htmlCompressor.setRemoveJavaScriptProtocol(removeJavaScriptProtocolOpt);
htmlCompressor.setRemoveHttpProtocol(removeHttpProtocolOpt);
htmlCompressor.setRemoveHttpsProtocol(removeHttpsProtocolOpt);
htmlCompressor.setRemoveSurroundingSpaces(removeSurroundingSpacesOpt);
htmlCompressor.setPreservePatterns(preservePatterns);
htmlCompressor.setYuiJsNoMunge(nomungeOpt);
htmlCompressor.setYuiJsPreserveAllSemiColons(preserveSemiOpt);
htmlCompressor.setYuiJsDisableOptimizations(disableOptimizationsOpt);
htmlCompressor.setYuiJsLineBreak(linebreakOpt);
htmlCompressor.setYuiCssLineBreak(linebreakOpt);
// switch js compressor to closure
if (compressJsOpt && useClosureCompressor) {
ClosureJavaScriptCompressor closureCompressor = new ClosureJavaScriptCompressor();
if (closureOptLevelOpt.equalsIgnoreCase(ClosureJavaScriptCompressor.COMPILATION_LEVEL_ADVANCED)) {
closureCompressor.setCompilationLevel(CompilationLevel.ADVANCED_OPTIMIZATIONS);
closureCompressor.setCustomExternsOnly(closureCustomExternsOnlyOpt);
// get externs
if (!closureExternsOpt.isEmpty()) {
List<SourceFile> externs = new ArrayList<>();
for (String externFile : closureExternsOpt) {
externs.add(SourceFile.fromFile(externFile));
}
closureCompressor.setExterns(externs);
}
} else if (closureOptLevelOpt.equalsIgnoreCase(ClosureJavaScriptCompressor.COMPILATION_LEVEL_WHITESPACE)) {
closureCompressor.setCompilationLevel(CompilationLevel.WHITESPACE_ONLY);
} else {
closureCompressor.setCompilationLevel(CompilationLevel.SIMPLE_OPTIMIZATIONS);
}
htmlCompressor.setJavaScriptCompressor(closureCompressor);
}
return htmlCompressor;
}
/**
* Creates the xml compressor.
*
* @return the compressor
*
* @throws IllegalArgumentException
* the illegal argument exception
* @throws OptionException
* the option exception
*/
private Compressor createXmlCompressor() throws IllegalArgumentException, OptionException {
XmlCompressor xmlCompressor = new XmlCompressor();
xmlCompressor.setRemoveComments(!preserveCommentsOpt);
xmlCompressor.setRemoveIntertagSpaces(!preserveIntertagSpacesOpt);
return xmlCompressor;
}
/**
* Builds the input output map.
*
* @return the map
*
* @throws IllegalArgumentException
* the illegal argument exception
* @throws IOException
* Signals that an I/O exception has occurred.
*/
private Map<String, String> buildInputOutputMap() throws IllegalArgumentException, IOException {
Map<String, String> map = new HashMap<>();
Path outputFile = null;
if (outputFilenameOpt != null) {
outputFile = Path.of(outputFilenameOpt);
// make dirs
if (outputFilenameOpt.endsWith("/") || outputFilenameOpt.endsWith("\\")) {
Files.createDirectories(outputFile);
} else {
Files.createDirectories(outputFile.toFile().getCanonicalFile().getParentFile().toPath());
}
}
if (fileArgsOpt.size() > 1 && (outputFile == null || !Files.isDirectory(outputFile))) {
throw new IllegalArgumentException("Output must be a directory and end with a slash (/)");
}
if (fileArgsOpt.isEmpty()) {
map.put(null, outputFilenameOpt);
} else {
for (int i = 0; i < fileArgsOpt.size(); i++) {
if (!urlPattern.matcher(fileArgsOpt.get(i)).matches()) {
Path inputFile = Path.of(fileArgsOpt.get(i));
if (Files.isDirectory(inputFile)) {
// is directory
if (outputFile != null && Files.isDirectory(outputFile)) {
if (!recursiveOpt) {
// non-recursive
for (File file : inputFile.toFile()
.listFiles(new CompressorFileFilter(typeOpt, filemaskOpt, false))) {
if (!file.isDirectory()) {
String from = file.getCanonicalPath();
String to = from.replaceFirst(escRegEx(inputFile.toFile().getCanonicalPath()),
Matcher.quoteReplacement(outputFile.toFile().getCanonicalPath()));
map.put(from, to);
}
}
} else {
// recursive
ArrayDeque<File> fileStack = new ArrayDeque<>();
fileStack.push(inputFile.toFile());
while (!fileStack.isEmpty()) {
File child = fileStack.pop();
if (child.isDirectory()) {
for (File f : child
.listFiles(new CompressorFileFilter(typeOpt, filemaskOpt, true))) {
fileStack.push(f);
}
} else if (child.isFile()) {
String from = child.getCanonicalPath();
String to = from.replaceFirst(escRegEx(inputFile.toFile().getCanonicalPath()),
Matcher.quoteReplacement(outputFile.toFile().getCanonicalPath()));
map.put(from, to);
// make directories
Files.createDirectories(
Path.of(to).toFile().getCanonicalFile().getParentFile().toPath());
}
}
}
} else {
throw new IllegalArgumentException("Output must be a directory and end with a slash (/)");
}
} else {
// is file
if (outputFile != null && Files.isDirectory(outputFile)) {
String from = inputFile.toFile().getCanonicalPath();
String to = from.replaceFirst(
escRegEx(inputFile.toFile().getCanonicalFile().getParentFile().getCanonicalPath()),
Matcher.quoteReplacement(outputFile.toFile().getCanonicalPath()));
map.put(fileArgsOpt.get(i), to);
} else {
map.put(fileArgsOpt.get(i), outputFilenameOpt);
}
}
} else {
// is url
if (fileArgsOpt.size() == 1 && (outputFile == null || !Files.isDirectory(outputFile))) {
map.put(fileArgsOpt.get(i), outputFilenameOpt);
} else {
throw new IllegalArgumentException(
"Input URL should be single and cannot have directory as output");
}
}
}
}
return map;
}
/**
* Builds the reader.
*
* @param filename
* the filename
*
* @return the buffered reader
*
* @throws IOException
* Signals that an I/O exception has occurred.
*/
private BufferedReader buildReader(String filename) throws IOException {
if (filename == null) {
return new BufferedReader(new InputStreamReader(System.in, charsetOpt));
} else if (urlPattern.matcher(filename).matches()) {
return new BufferedReader(
new InputStreamReader(URI.create(filename).toURL().openConnection().getInputStream(), charsetOpt));
} else {
return Files.newBufferedReader(Path.of(filename), charsetOpt);
}
}
/**
* Builds the writer.
*
* @param filename
* the filename
*
* @return the writer
*
* @throws IOException
* Signals that an I/O exception has occurred.
*/
private Writer buildWriter(String filename) throws IOException {
if (filename == null) {
return new OutputStreamWriter(System.out, charsetOpt);
} else {
return new OutputStreamWriter(Files.newOutputStream(Path.of(filename)), charsetOpt);
}
}
/**
* Read resource.
*
* @param input
* the input
*
* @return the string
*
* @throws IOException
* Signals that an I/O exception has occurred.
*/
private String readResource(BufferedReader input) throws IOException {
StringBuilder source = new StringBuilder();
try {
String line = null;
while ((line = input.readLine()) != null) {
source.append(line);
source.append(System.getProperty("line.separator"));
}
} finally {
closeStream(input);
}
return source.toString();
}
/**
* Write resource.
*
* @param content
* the content
* @param output
* the output
*
* @throws IOException
* Signals that an I/O exception has occurred.
*/
private void writeResource(String content, Writer output) throws IOException {
try {
output.write(content);
} finally {
closeStream(output);
}
}
/**
* Close stream.
*
* @param stream
* the stream
*/
private void closeStream(Closeable stream) {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
logger.trace("", e);
}
}
}
/**
* Esc reg ex.
*
* @param inStr
* the in str
*
* @return the string
*/
private String escRegEx(String inStr) {
return inStr.replaceAll("([\\\\*+\\[\\](){}\\$.?\\^|])", "\\\\$1");
}
/**
* Prints the usage.
*/
private void printUsage() {
logger.info("""
Usage: java -jar htmlcompressor.jar [options] [input]
[input] URL, filename, directory, or space separated list
of files and directories to compress.
If none provided reads from <stdin>
Global Options:
-?, /?, -h, --help Displays this help screen
-t, --type <html|xml> If not provided autodetects from file extension
-r, --recursive Process files inside subdirectories
-c, --charset <charset> Charset for reading files, UTF-8 by default
-m, --mask <filemask> Filter input files inside directories by mask
-o, --output <path> Filename or directory for compression results.
If none provided outputs result to <stdout>
-a, --analyze Tries different settings and displays report.
All settings except --js-compressor are ignored
XML Compression Options:
--preserve-comments Preserve comments
--preserve-intertag-spaces Preserve intertag spaces
HTML Compression Options:
--preserve-comments Preserve comments
--preserve-multi-spaces Preserve multiple spaces
--preserve-line-breaks Preserve line breaks
--remove-intertag-spaces Remove intertag spaces
--remove-quotes Remove unneeded quotes
--simple-doctype Change doctype to <!DOCTYPE html>
--remove-style-attr Remove TYPE attribute from STYLE tags
--remove-link-attr Remove TYPE attribute from LINK tags
--remove-script-attr Remove TYPE and LANGUAGE from SCRIPT tags
--remove-form-attr Remove METHOD=\"GET\" from FORM tags
--remove-input-attr Remove TYPE=\"TEXT\" from INPUT tags
--simple-bool-attr Remove values from boolean tag attributes
--remove-js-protocol Remove \"javascript:\" from inline event handlers
--remove-http-protocol Remove \"http:\" from tag attributes
--remove-https-protocol Remove \"https:\" from tag attributes
--remove-surrounding-spaces <min|max|all|custom_list>
Predefined or custom comma separated list of tags
--compress-js Enable inline JavaScript compression
--compress-css Enable inline CSS compression using YUICompressor
--js-compressor <yui|closure> Switch inline JavaScript compressor between
YUICompressor (default) and Closure Compiler
JavaScript Compression Options for YUI Compressor:
--nomunge Minify only, do not obfuscate
--preserve-semi Preserve all semicolons
--disable-optimizations Disable all micro optimizations
--line-break <column num> Insert a line break after the specified column
JavaScript Compression Options for Google Closure Compiler:
--closure-opt-level <simple|advanced|whitespace>
Sets level of optimization (simple by default)
--closure-externs <file> Sets custom externs file, repeat for each file
--closure-custom-externs-only Disable default built-in externs
CSS Compression Options for YUI Compressor:
--line-break <column num> Insert a line break after the specified column
Custom Block Preservation Options:
--preserve-php Preserve <?php ... ?> tags
--preserve-server-script Preserve <% ... %> tags
--preserve-ssi Preserve <!--# ... --> tags
-p, --preserve <path> Read regular expressions that define
custom preservation rules from a file
Please note that if you enable CSS or JavaScript compression, additional
YUI Compressor or Google Closure Compiler jar files must be present
in the same directory as this jar.
""");
}
/**
* The Class CompressorFileFilter.
*/
private class CompressorFileFilter implements FileFilter {
/** The filemask pattern. */
private Pattern filemaskPattern;
/** The with dirs. */
private boolean withDirs;
/**
* Instantiates a new compressor file filter.
*
* @param type
* the type
* @param filemask
* the filemask
* @param withDirs
* the with dirs
*/
public CompressorFileFilter(String type, String filemask, boolean withDirs) {
this.withDirs = withDirs;
if (filemask == null) {
if (type != null && "xml".equalsIgnoreCase(type)) {
filemaskPattern = Pattern.compile("^.*\\.xml$", Pattern.CASE_INSENSITIVE);
} else {
filemaskPattern = Pattern.compile("^.*\\.html?$", Pattern.CASE_INSENSITIVE);
}
} else {
// turn mask into regexp
filemask = filemask.replaceAll(escRegEx("."), Matcher.quoteReplacement("\\."));
filemask = filemask.replaceAll(escRegEx("*"), Matcher.quoteReplacement(".*"));
filemask = filemask.replaceAll(escRegEx("?"), Matcher.quoteReplacement("."));
filemask = filemask.replaceAll(escRegEx(";"), Matcher.quoteReplacement("$|^"));
filemask = "^" + filemask + "$";
filemaskPattern = Pattern.compile(filemask, Pattern.CASE_INSENSITIVE);
}
}
@Override
public boolean accept(File file) {
if (!withDirs) {
// take only matching non-dirs
if (!file.isDirectory()) {
return filemaskPattern.matcher(file.getName()).matches();
}
} else {
// take matching files and dirs
return file.isDirectory() || filemaskPattern.matcher(file.getName()).matches();
}
return false;
}
}
}