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