1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package com.googlecode.htmlcompressor;
17
18 import com.google.javascript.jscomp.CompilationLevel;
19 import com.google.javascript.jscomp.SourceFile;
20 import com.googlecode.htmlcompressor.analyzer.HtmlAnalyzer;
21 import com.googlecode.htmlcompressor.compressor.ClosureJavaScriptCompressor;
22 import com.googlecode.htmlcompressor.compressor.Compressor;
23 import com.googlecode.htmlcompressor.compressor.HtmlCompressor;
24 import com.googlecode.htmlcompressor.compressor.XmlCompressor;
25
26 import java.io.BufferedReader;
27 import java.io.Closeable;
28 import java.io.File;
29 import java.io.FileFilter;
30 import java.io.IOException;
31 import java.io.InputStreamReader;
32 import java.io.OutputStreamWriter;
33 import java.io.Writer;
34 import java.net.URI;
35 import java.nio.charset.Charset;
36 import java.nio.charset.StandardCharsets;
37 import java.nio.file.Files;
38 import java.nio.file.Path;
39 import java.util.ArrayDeque;
40 import java.util.ArrayList;
41 import java.util.HashMap;
42 import java.util.List;
43 import java.util.Locale;
44 import java.util.Map;
45 import java.util.regex.Matcher;
46 import java.util.regex.Pattern;
47 import java.util.regex.PatternSyntaxException;
48
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51
52 import jargs.gnu.CmdLineParser;
53 import jargs.gnu.CmdLineParser.Option;
54 import jargs.gnu.CmdLineParser.OptionException;
55
56
57
58
59
60
61
62
63
64
65 public class CmdLineCompressor {
66
67
68 private static final Logger logger = LoggerFactory.getLogger(CmdLineCompressor.class);
69
70
71 private static final Pattern urlPattern = Pattern.compile("^https?://.*$", Pattern.CASE_INSENSITIVE);
72
73
74 private boolean helpOpt;
75
76
77 private boolean analyzeOpt;
78
79
80 private Charset charsetOpt;
81
82
83 private String outputFilenameOpt;
84
85
86 private String patternsFilenameOpt;
87
88
89 private String typeOpt;
90
91
92 private String filemaskOpt;
93
94
95 private boolean recursiveOpt;
96
97
98 private boolean preserveCommentsOpt;
99
100
101 private boolean preserveIntertagSpacesOpt;
102
103
104 private boolean preserveMultiSpacesOpt;
105
106
107 private boolean removeIntertagSpacesOpt;
108
109
110 private boolean removeQuotesOpt;
111
112
113 private String removeSurroundingSpacesOpt;
114
115
116 private boolean preserveLineBreaksOpt;
117
118
119 private boolean preservePhpTagsOpt;
120
121
122 private boolean preserveServerScriptTagsOpt;
123
124
125 private boolean preserveSsiTagsOpt;
126
127
128 private boolean compressJsOpt;
129
130
131 private boolean compressCssOpt;
132
133
134 private String jsCompressorOpt;
135
136
137 private boolean simpleDoctypeOpt;
138
139
140 private boolean removeScriptAttributesOpt;
141
142
143 private boolean removeStyleAttributesOpt;
144
145
146 private boolean removeLinkAttributesOpt;
147
148
149 private boolean removeFormAttributesOpt;
150
151
152 private boolean removeInputAttributesOpt;
153
154
155 private boolean simpleBooleanAttributesOpt;
156
157
158 private boolean removeJavaScriptProtocolOpt;
159
160
161 private boolean removeHttpProtocolOpt;
162
163
164 private boolean removeHttpsProtocolOpt;
165
166
167 private boolean nomungeOpt;
168
169
170 private int linebreakOpt;
171
172
173 private boolean preserveSemiOpt;
174
175
176 private boolean disableOptimizationsOpt;
177
178
179 private String closureOptLevelOpt;
180
181
182 private boolean closureCustomExternsOnlyOpt;
183
184
185 private List<String> closureExternsOpt;
186
187
188 private List<String> fileArgsOpt;
189
190
191
192
193
194
195
196 public CmdLineCompressor(String[] args) {
197 CmdLineParser parser = new CmdLineParser();
198
199 Option helpOption = parser.addBooleanOption('h', "help");
200 Option helpOptAlt = parser.addBooleanOption('?', "help_alt");
201 Option analyzeOption = parser.addBooleanOption('a', "analyze");
202 Option recursiveOption = parser.addBooleanOption('r', "recursive");
203 Option charsetOption = parser.addStringOption('c', "charset");
204 Option outputFilenameOption = parser.addStringOption('o', "output");
205 Option patternsFilenameOption = parser.addStringOption('p', "preserve");
206 Option typeOption = parser.addStringOption('t', "type");
207 Option filemaskOption = parser.addStringOption('m', "mask");
208 Option preserveCommentsOption = parser.addBooleanOption("preserve-comments");
209 Option preserveIntertagSpacesOption = parser.addBooleanOption("preserve-intertag-spaces");
210 Option preserveMultiSpacesOption = parser.addBooleanOption("preserve-multi-spaces");
211 Option removeIntertagSpacesOption = parser.addBooleanOption("remove-intertag-spaces");
212 Option removeSurroundingSpacesOption = parser.addStringOption("remove-surrounding-spaces");
213 Option removeQuotesOption = parser.addBooleanOption("remove-quotes");
214 Option preserveLineBreaksOption = parser.addBooleanOption("preserve-line-breaks");
215 Option preservePhpTagsOption = parser.addBooleanOption("preserve-php");
216 Option preserveServerScriptTagsOption = parser.addBooleanOption("preserve-server-script");
217 Option preserveSsiTagsOption = parser.addBooleanOption("preserve-ssi");
218 Option compressJsOption = parser.addBooleanOption("compress-js");
219 Option compressCssOption = parser.addBooleanOption("compress-css");
220 Option jsCompressorOption = parser.addStringOption("js-compressor");
221
222 Option simpleDoctypeOption = parser.addBooleanOption("simple-doctype");
223 Option removeScriptAttributesOption = parser.addBooleanOption("remove-script-attr");
224 Option removeStyleAttributesOption = parser.addBooleanOption("remove-style-attr");
225 Option removeLinkAttributesOption = parser.addBooleanOption("remove-link-attr");
226 Option removeFormAttributesOption = parser.addBooleanOption("remove-form-attr");
227 Option removeInputAttributesOption = parser.addBooleanOption("remove-input-attr");
228 Option simpleBooleanAttributesOption = parser.addBooleanOption("simple-bool-attr");
229 Option removeJavaScriptProtocolOption = parser.addBooleanOption("remove-js-protocol");
230 Option removeHttpProtocolOption = parser.addBooleanOption("remove-http-protocol");
231 Option removeHttpsProtocolOption = parser.addBooleanOption("remove-https-protocol");
232
233 Option nomungeOption = parser.addBooleanOption("nomunge");
234 Option linebreakOption = parser.addStringOption("line-break");
235 Option preserveSemiOption = parser.addBooleanOption("preserve-semi");
236 Option disableOptimizationsOption = parser.addBooleanOption("disable-optimizations");
237
238 Option closureOptLevelOption = parser.addStringOption("closure-opt-level");
239 Option closureCustomExternsOnlyOption = parser.addBooleanOption("closure-custom-externs-only");
240 Option closureExternsOption = parser.addStringOption("closure-externs");
241
242 try {
243 parser.parse(args);
244
245 this.helpOpt = (Boolean) parser.getOptionValue(helpOption, false)
246 || (Boolean) parser.getOptionValue(helpOptAlt, false);
247 this.analyzeOpt = (Boolean) parser.getOptionValue(analyzeOption, false);
248 this.recursiveOpt = (Boolean) parser.getOptionValue(recursiveOption, false);
249 this.charsetOpt = Charset.forName((String) parser.getOptionValue(charsetOption, "UTF-8"));
250 this.outputFilenameOpt = (String) parser.getOptionValue(outputFilenameOption);
251 this.patternsFilenameOpt = (String) parser.getOptionValue(patternsFilenameOption);
252 this.typeOpt = (String) parser.getOptionValue(typeOption);
253 this.filemaskOpt = (String) parser.getOptionValue(filemaskOption);
254 this.preserveCommentsOpt = (Boolean) parser.getOptionValue(preserveCommentsOption, false);
255 this.preserveIntertagSpacesOpt = (Boolean) parser.getOptionValue(preserveIntertagSpacesOption, false);
256 this.preserveMultiSpacesOpt = (Boolean) parser.getOptionValue(preserveMultiSpacesOption, false);
257 this.removeIntertagSpacesOpt = (Boolean) parser.getOptionValue(removeIntertagSpacesOption, false);
258 this.removeQuotesOpt = (Boolean) parser.getOptionValue(removeQuotesOption, false);
259 this.preserveLineBreaksOpt = (Boolean) parser.getOptionValue(preserveLineBreaksOption, false);
260 this.preservePhpTagsOpt = (Boolean) parser.getOptionValue(preservePhpTagsOption, false);
261 this.preserveServerScriptTagsOpt = (Boolean) parser.getOptionValue(preserveServerScriptTagsOption, false);
262 this.preserveSsiTagsOpt = (Boolean) parser.getOptionValue(preserveSsiTagsOption, false);
263 this.compressJsOpt = (Boolean) parser.getOptionValue(compressJsOption, false);
264 this.compressCssOpt = (Boolean) parser.getOptionValue(compressCssOption, false);
265 this.jsCompressorOpt = (String) parser.getOptionValue(jsCompressorOption, HtmlCompressor.JS_COMPRESSOR_YUI);
266
267 this.simpleDoctypeOpt = (Boolean) parser.getOptionValue(simpleDoctypeOption, false);
268 this.removeScriptAttributesOpt = (Boolean) parser.getOptionValue(removeScriptAttributesOption, false);
269 this.removeStyleAttributesOpt = (Boolean) parser.getOptionValue(removeStyleAttributesOption, false);
270 this.removeLinkAttributesOpt = (Boolean) parser.getOptionValue(removeLinkAttributesOption, false);
271 this.removeFormAttributesOpt = (Boolean) parser.getOptionValue(removeFormAttributesOption, false);
272 this.removeInputAttributesOpt = (Boolean) parser.getOptionValue(removeInputAttributesOption, false);
273 this.simpleBooleanAttributesOpt = (Boolean) parser.getOptionValue(simpleBooleanAttributesOption, false);
274 this.removeJavaScriptProtocolOpt = (Boolean) parser.getOptionValue(removeJavaScriptProtocolOption, false);
275 this.removeHttpProtocolOpt = (Boolean) parser.getOptionValue(removeHttpProtocolOption, false);
276 this.removeHttpsProtocolOpt = (Boolean) parser.getOptionValue(removeHttpsProtocolOption, false);
277
278 this.nomungeOpt = (Boolean) parser.getOptionValue(nomungeOption, false);
279 this.linebreakOpt = (Integer) parser.getOptionValue(linebreakOption, -1);
280 this.preserveSemiOpt = (Boolean) parser.getOptionValue(preserveSemiOption, false);
281 this.disableOptimizationsOpt = (Boolean) parser.getOptionValue(disableOptimizationsOption, false);
282
283 this.closureOptLevelOpt = (String) parser.getOptionValue(closureOptLevelOption,
284 ClosureJavaScriptCompressor.COMPILATION_LEVEL_SIMPLE);
285 this.closureCustomExternsOnlyOpt = (Boolean) parser.getOptionValue(closureCustomExternsOnlyOption, false);
286
287 this.closureExternsOpt = parser.getOptionValues(closureExternsOption);
288
289 this.removeSurroundingSpacesOpt = (String) parser.getOptionValue(removeSurroundingSpacesOption);
290 if (this.removeSurroundingSpacesOpt != null) {
291 if ("min".equalsIgnoreCase(this.removeSurroundingSpacesOpt)) {
292 this.removeSurroundingSpacesOpt = HtmlCompressor.BLOCK_TAGS_MIN;
293 } else if ("max".equalsIgnoreCase(this.removeSurroundingSpacesOpt)) {
294 this.removeSurroundingSpacesOpt = HtmlCompressor.BLOCK_TAGS_MAX;
295 } else if ("all".equalsIgnoreCase(this.removeSurroundingSpacesOpt)) {
296 this.removeSurroundingSpacesOpt = HtmlCompressor.ALL_TAGS;
297 }
298 }
299
300
301 this.fileArgsOpt = parser.getRemainingArgs();
302
303
304 this.charsetOpt = Charset.isSupported(this.charsetOpt.name()) ? this.charsetOpt : StandardCharsets.UTF_8;
305
306
307 for (int i = 0; i < args.length; i++) {
308 if ("/?".equals(args[i])) {
309 this.helpOpt = true;
310 break;
311 }
312 }
313
314 } catch (OptionException e) {
315 logger.info("{}", e.getMessage());
316 logger.trace("", e);
317 printUsage();
318 }
319
320 }
321
322
323
324
325
326
327
328 public static void main(String[] args) {
329 CmdLineCompressor cmdLineCompressor = new CmdLineCompressor(args);
330 cmdLineCompressor.process();
331 }
332
333
334
335
336 public void process() {
337 try {
338
339
340 if (helpOpt) {
341 printUsage();
342 return;
343 }
344
345
346 String type = typeOpt;
347 if (type != null && !"html".equalsIgnoreCase(type) && !"xml".equalsIgnoreCase(type)) {
348 throw new IllegalArgumentException("Unknown type: " + type);
349 }
350
351 if (fileArgsOpt.isEmpty()) {
352
353 if (type == null) {
354 type = "html";
355 }
356 } else if (type == null) {
357
358 if (fileArgsOpt.get(0).toLowerCase(Locale.ENGLISH).endsWith(".xml")) {
359 type = "xml";
360 } else {
361 type = "html";
362 }
363 }
364
365 if (analyzeOpt) {
366
367 HtmlAnalyzer analyzer = new HtmlAnalyzer(
368 HtmlCompressor.JS_COMPRESSOR_CLOSURE.equalsIgnoreCase(jsCompressorOpt)
369 ? HtmlCompressor.JS_COMPRESSOR_CLOSURE
370 : HtmlCompressor.JS_COMPRESSOR_YUI);
371 analyzer.analyze(readResource(buildReader(fileArgsOpt.isEmpty() ? null : fileArgsOpt.get(0))));
372 } else {
373
374 Compressor compressor = "xml".equalsIgnoreCase(type) ? createXmlCompressor() : createHtmlCompressor();
375 Map<String, String> ioMap = buildInputOutputMap();
376 for (Map.Entry<String, String> entry : ioMap.entrySet()) {
377 writeResource(compressor.compress(readResource(buildReader(entry.getKey()))),
378 buildWriter(entry.getValue()));
379 }
380 }
381
382 } catch (NoClassDefFoundError e) {
383 if (HtmlCompressor.JS_COMPRESSOR_CLOSURE.equalsIgnoreCase(jsCompressorOpt)) {
384 logger.info("""
385 ERROR: For JavaScript compression using Google Closure Compiler
386 additional jar file called compiler.jar must be present
387 in the same directory as HtmlCompressor jar
388 """);
389 } else {
390 logger.info("""
391 ERROR: For CSS or JavaScript compression using YUICompressor additional jar file
392 called yuicompressor.jar must be present in the same directory as HtmlCompressor jar
393 """);
394 }
395 logger.trace("", e);
396 } catch (IOException | IllegalArgumentException e) {
397 logger.info("{}", e.getMessage());
398 logger.trace("", e);
399 }
400
401 }
402
403
404
405
406
407
408 private Compressor createHtmlCompressor() {
409
410 boolean useClosureCompressor = HtmlCompressor.JS_COMPRESSOR_CLOSURE.equalsIgnoreCase(jsCompressorOpt);
411
412
413 List<Pattern> preservePatterns = new ArrayList<>();
414
415
416 if (preservePhpTagsOpt) {
417 preservePatterns.add(HtmlCompressor.PHP_TAG_PATTERN);
418 }
419
420 if (preserveServerScriptTagsOpt) {
421 preservePatterns.add(HtmlCompressor.SERVER_SCRIPT_TAG_PATTERN);
422 }
423
424 if (preserveSsiTagsOpt) {
425 preservePatterns.add(HtmlCompressor.SERVER_SIDE_INCLUDE_PATTERN);
426 }
427
428 if (patternsFilenameOpt != null) {
429
430 try (BufferedReader patternsIn = Files.newBufferedReader(Path.of(patternsFilenameOpt), charsetOpt)) {
431
432 String line = null;
433 while ((line = patternsIn.readLine()) != null) {
434 if (!line.isEmpty()) {
435 try {
436 preservePatterns.add(Pattern.compile(line));
437 } catch (PatternSyntaxException e) {
438 logger.trace("", e);
439 throw new IllegalArgumentException(
440 "Regular expression compilation error: " + e.getMessage());
441 }
442 }
443 }
444 } catch (IOException e) {
445 logger.trace("", e);
446 throw new IllegalArgumentException("Unable to read custom pattern definitions file: " + e.getMessage());
447 }
448 }
449
450
451 HtmlCompressor htmlCompressor = new HtmlCompressor();
452
453 htmlCompressor.setRemoveComments(!preserveCommentsOpt);
454 htmlCompressor.setRemoveMultiSpaces(!preserveMultiSpacesOpt);
455 htmlCompressor.setRemoveIntertagSpaces(removeIntertagSpacesOpt);
456 htmlCompressor.setRemoveQuotes(removeQuotesOpt);
457 htmlCompressor.setPreserveLineBreaks(preserveLineBreaksOpt);
458 htmlCompressor.setCompressJavaScript(compressJsOpt);
459 htmlCompressor.setCompressCss(compressCssOpt);
460
461 htmlCompressor.setSimpleDoctype(simpleDoctypeOpt);
462 htmlCompressor.setRemoveScriptAttributes(removeScriptAttributesOpt);
463 htmlCompressor.setRemoveStyleAttributes(removeStyleAttributesOpt);
464 htmlCompressor.setRemoveLinkAttributes(removeLinkAttributesOpt);
465 htmlCompressor.setRemoveFormAttributes(removeFormAttributesOpt);
466 htmlCompressor.setRemoveInputAttributes(removeInputAttributesOpt);
467 htmlCompressor.setSimpleBooleanAttributes(simpleBooleanAttributesOpt);
468 htmlCompressor.setRemoveJavaScriptProtocol(removeJavaScriptProtocolOpt);
469 htmlCompressor.setRemoveHttpProtocol(removeHttpProtocolOpt);
470 htmlCompressor.setRemoveHttpsProtocol(removeHttpsProtocolOpt);
471 htmlCompressor.setRemoveSurroundingSpaces(removeSurroundingSpacesOpt);
472
473 htmlCompressor.setPreservePatterns(preservePatterns);
474
475 htmlCompressor.setYuiJsNoMunge(nomungeOpt);
476 htmlCompressor.setYuiJsPreserveAllSemiColons(preserveSemiOpt);
477 htmlCompressor.setYuiJsDisableOptimizations(disableOptimizationsOpt);
478 htmlCompressor.setYuiJsLineBreak(linebreakOpt);
479 htmlCompressor.setYuiCssLineBreak(linebreakOpt);
480
481
482 if (compressJsOpt && useClosureCompressor) {
483 ClosureJavaScriptCompressor closureCompressor = new ClosureJavaScriptCompressor();
484
485 if (closureOptLevelOpt.equalsIgnoreCase(ClosureJavaScriptCompressor.COMPILATION_LEVEL_ADVANCED)) {
486 closureCompressor.setCompilationLevel(CompilationLevel.ADVANCED_OPTIMIZATIONS);
487 closureCompressor.setCustomExternsOnly(closureCustomExternsOnlyOpt);
488
489
490 if (!closureExternsOpt.isEmpty()) {
491 List<SourceFile> externs = new ArrayList<>();
492 for (String externFile : closureExternsOpt) {
493 externs.add(SourceFile.fromFile(externFile));
494 }
495 closureCompressor.setExterns(externs);
496 }
497 } else if (closureOptLevelOpt.equalsIgnoreCase(ClosureJavaScriptCompressor.COMPILATION_LEVEL_WHITESPACE)) {
498 closureCompressor.setCompilationLevel(CompilationLevel.WHITESPACE_ONLY);
499 } else {
500 closureCompressor.setCompilationLevel(CompilationLevel.SIMPLE_OPTIMIZATIONS);
501 }
502
503 htmlCompressor.setJavaScriptCompressor(closureCompressor);
504 }
505
506 return htmlCompressor;
507 }
508
509
510
511
512
513
514
515
516
517 private Compressor createXmlCompressor() throws IllegalArgumentException {
518 XmlCompressor xmlCompressor = new XmlCompressor();
519 xmlCompressor.setRemoveComments(!preserveCommentsOpt);
520 xmlCompressor.setRemoveIntertagSpaces(!preserveIntertagSpacesOpt);
521
522 return xmlCompressor;
523 }
524
525
526
527
528
529
530
531
532
533
534
535 private Map<String, String> buildInputOutputMap() throws IllegalArgumentException, IOException {
536 Map<String, String> map = new HashMap<>();
537
538 Path outputFile = null;
539 if (outputFilenameOpt != null) {
540 outputFile = Path.of(outputFilenameOpt);
541
542
543 if (outputFilenameOpt.endsWith("/") || outputFilenameOpt.endsWith("\\")) {
544 Files.createDirectories(outputFile);
545 } else {
546 Files.createDirectories(outputFile.toFile().getCanonicalFile().getParentFile().toPath());
547 }
548 }
549
550 if (fileArgsOpt.size() > 1 && (outputFile == null || !Files.isDirectory(outputFile))) {
551 throw new IllegalArgumentException("Output must be a directory and end with a slash (/)");
552 }
553
554 if (fileArgsOpt.isEmpty()) {
555 map.put(null, outputFilenameOpt);
556 } else {
557 for (int i = 0; i < fileArgsOpt.size(); i++) {
558 if (!urlPattern.matcher(fileArgsOpt.get(i)).matches()) {
559 Path inputFile = Path.of(fileArgsOpt.get(i));
560 if (Files.isDirectory(inputFile)) {
561
562 if (outputFile != null && Files.isDirectory(outputFile)) {
563 if (!recursiveOpt) {
564
565 for (File file : inputFile.toFile()
566 .listFiles(new CompressorFileFilter(typeOpt, filemaskOpt, false))) {
567 if (!file.isDirectory()) {
568 String from = file.getCanonicalPath();
569 String to = from.replaceFirst(escRegEx(inputFile.toFile().getCanonicalPath()),
570 Matcher.quoteReplacement(outputFile.toFile().getCanonicalPath()));
571 map.put(from, to);
572 }
573 }
574 } else {
575
576 ArrayDeque<File> fileStack = new ArrayDeque<>();
577 fileStack.push(inputFile.toFile());
578 while (!fileStack.isEmpty()) {
579 File child = fileStack.pop();
580 if (child.isDirectory()) {
581 for (File f : child
582 .listFiles(new CompressorFileFilter(typeOpt, filemaskOpt, true))) {
583 fileStack.push(f);
584 }
585 } else if (child.isFile()) {
586 String from = child.getCanonicalPath();
587 String to = from.replaceFirst(escRegEx(inputFile.toFile().getCanonicalPath()),
588 Matcher.quoteReplacement(outputFile.toFile().getCanonicalPath()));
589 map.put(from, to);
590
591 Files.createDirectories(
592 Path.of(to).toFile().getCanonicalFile().getParentFile().toPath());
593 }
594 }
595 }
596 } else {
597 throw new IllegalArgumentException("Output must be a directory and end with a slash (/)");
598 }
599 } else {
600
601 if (outputFile != null && Files.isDirectory(outputFile)) {
602 String from = inputFile.toFile().getCanonicalPath();
603 String to = from.replaceFirst(
604 escRegEx(inputFile.toFile().getCanonicalFile().getParentFile().getCanonicalPath()),
605 Matcher.quoteReplacement(outputFile.toFile().getCanonicalPath()));
606 map.put(fileArgsOpt.get(i), to);
607 } else {
608 map.put(fileArgsOpt.get(i), outputFilenameOpt);
609 }
610
611 }
612 } else {
613
614 if (fileArgsOpt.size() == 1 && (outputFile == null || !Files.isDirectory(outputFile))) {
615 map.put(fileArgsOpt.get(i), outputFilenameOpt);
616 } else {
617 throw new IllegalArgumentException(
618 "Input URL should be single and cannot have directory as output");
619 }
620 }
621 }
622 }
623
624 return map;
625 }
626
627
628
629
630
631
632
633
634
635
636
637
638 private BufferedReader buildReader(String filename) throws IOException {
639 if (filename == null) {
640 return new BufferedReader(new InputStreamReader(System.in, charsetOpt));
641 } else if (urlPattern.matcher(filename).matches()) {
642 return new BufferedReader(
643 new InputStreamReader(URI.create(filename).toURL().openConnection().getInputStream(), charsetOpt));
644 } else {
645 return Files.newBufferedReader(Path.of(filename), charsetOpt);
646 }
647 }
648
649
650
651
652
653
654
655
656
657
658
659
660 private Writer buildWriter(String filename) throws IOException {
661 if (filename == null) {
662 return new OutputStreamWriter(System.out, charsetOpt);
663 } else {
664 return new OutputStreamWriter(Files.newOutputStream(Path.of(filename)), charsetOpt);
665 }
666 }
667
668
669
670
671
672
673
674
675
676
677
678
679 private String readResource(BufferedReader input) throws IOException {
680 StringBuilder source = new StringBuilder();
681 try {
682 String line = null;
683 while ((line = input.readLine()) != null) {
684 source.append(line);
685 source.append(System.getProperty("line.separator"));
686 }
687 } finally {
688 closeStream(input);
689 }
690 return source.toString();
691 }
692
693
694
695
696
697
698
699
700
701
702
703
704 private void writeResource(String content, Writer output) throws IOException {
705 try {
706 output.write(content);
707 } finally {
708 closeStream(output);
709 }
710 }
711
712
713
714
715
716
717
718 private void closeStream(Closeable stream) {
719 if (stream != null) {
720 try {
721 stream.close();
722 } catch (IOException e) {
723 logger.trace("", e);
724 }
725 }
726 }
727
728
729
730
731
732
733
734
735
736 private String escRegEx(String inStr) {
737 return inStr.replaceAll("([\\\\*+\\[\\](){}\\$.?\\^|])", "\\\\$1");
738 }
739
740
741
742
743 private void printUsage() {
744 logger.info("""
745 Usage: java -jar htmlcompressor.jar [options] [input]
746
747 [input] URL, filename, directory, or space separated list
748 of files and directories to compress.
749 If none provided reads from <stdin>
750
751 Global Options:
752 -?, /?, -h, --help Displays this help screen
753 -t, --type <html|xml> If not provided autodetects from file extension
754 -r, --recursive Process files inside subdirectories
755 -c, --charset <charset> Charset for reading files, UTF-8 by default
756 -m, --mask <filemask> Filter input files inside directories by mask
757 -o, --output <path> Filename or directory for compression results.
758 If none provided outputs result to <stdout>
759 -a, --analyze Tries different settings and displays report.
760 All settings except --js-compressor are ignored
761
762 XML Compression Options:
763 --preserve-comments Preserve comments
764 --preserve-intertag-spaces Preserve intertag spaces
765
766 HTML Compression Options:
767 --preserve-comments Preserve comments
768 --preserve-multi-spaces Preserve multiple spaces
769 --preserve-line-breaks Preserve line breaks
770 --remove-intertag-spaces Remove intertag spaces
771 --remove-quotes Remove unneeded quotes
772 --simple-doctype Change doctype to <!DOCTYPE html>
773 --remove-style-attr Remove TYPE attribute from STYLE tags
774 --remove-link-attr Remove TYPE attribute from LINK tags
775 --remove-script-attr Remove TYPE and LANGUAGE from SCRIPT tags
776 --remove-form-attr Remove METHOD="GET" from FORM tags
777 --remove-input-attr Remove TYPE="TEXT" from INPUT tags
778 --simple-bool-attr Remove values from boolean tag attributes
779 --remove-js-protocol Remove "javascript:" from inline event handlers
780 --remove-http-protocol Remove "http:" from tag attributes
781 --remove-https-protocol Remove "https:" from tag attributes
782 --remove-surrounding-spaces <min|max|all|custom_list>
783 Predefined or custom comma separated list of tags
784 --compress-js Enable inline JavaScript compression
785 --compress-css Enable inline CSS compression using YUICompressor
786 --js-compressor <yui|closure> Switch inline JavaScript compressor between
787 YUICompressor (default) and Closure Compiler
788
789 JavaScript Compression Options for YUI Compressor:
790 --nomunge Minify only, do not obfuscate
791 --preserve-semi Preserve all semicolons
792 --disable-optimizations Disable all micro optimizations
793 --line-break <column num> Insert a line break after the specified column
794
795 JavaScript Compression Options for Google Closure Compiler:
796 --closure-opt-level <simple|advanced|whitespace>
797 Sets level of optimization (simple by default)
798 --closure-externs <file> Sets custom externs file, repeat for each file
799 --closure-custom-externs-only Disable default built-in externs
800
801 CSS Compression Options for YUI Compressor:
802 --line-break <column num> Insert a line break after the specified column
803
804 Custom Block Preservation Options:
805 --preserve-php Preserve <?php ... ?> tags
806 --preserve-server-script Preserve <% ... %> tags
807 --preserve-ssi Preserve <!--# ... --> tags
808 -p, --preserve <path> Read regular expressions that define
809 custom preservation rules from a file
810
811 Please note that if you enable CSS or JavaScript compression, additional
812 YUI Compressor or Google Closure Compiler jar files must be present
813 in the same directory as this jar.
814 """);
815 }
816
817
818
819
820 private class CompressorFileFilter implements FileFilter {
821
822
823 private Pattern filemaskPattern;
824
825
826 private boolean withDirs;
827
828
829
830
831
832
833
834
835
836
837
838 CompressorFileFilter(String type, String filemask, boolean withDirs) {
839
840 this.withDirs = withDirs;
841
842 if (filemask == null) {
843 if (type != null && "xml".equalsIgnoreCase(type)) {
844 filemaskPattern = Pattern.compile("^.*\\.xml$", Pattern.CASE_INSENSITIVE);
845 } else {
846 filemaskPattern = Pattern.compile("^.*\\.html?$", Pattern.CASE_INSENSITIVE);
847 }
848 } else {
849
850 filemask = filemask.replaceAll(escRegEx("."), Matcher.quoteReplacement("\\."));
851 filemask = filemask.replaceAll(escRegEx("*"), Matcher.quoteReplacement(".*"));
852 filemask = filemask.replaceAll(escRegEx("?"), Matcher.quoteReplacement("."));
853 filemask = filemask.replaceAll(escRegEx(";"), Matcher.quoteReplacement("$|^"));
854 filemask = "^" + filemask + "$";
855
856 filemaskPattern = Pattern.compile(filemask, Pattern.CASE_INSENSITIVE);
857 }
858 }
859
860 @Override
861 public boolean accept(File file) {
862 if (!withDirs) {
863
864 if (!file.isDirectory()) {
865 return filemaskPattern.matcher(file.getName()).matches();
866 }
867 } else {
868
869 return file.isDirectory() || filemaskPattern.matcher(file.getName()).matches();
870 }
871 return false;
872 }
873
874 }
875
876 }