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.FileInputStream;
31 import java.io.FileOutputStream;
32 import java.io.IOException;
33 import java.io.InputStreamReader;
34 import java.io.OutputStreamWriter;
35 import java.io.Writer;
36 import java.net.URL;
37 import java.nio.charset.Charset;
38 import java.nio.charset.StandardCharsets;
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 : HtmlCompressor.JS_COMPRESSOR_YUI);
372 analyzer.analyze(readResource(buildReader(fileArgsOpt.isEmpty() ? null : fileArgsOpt.get(0))));
373 } else {
374
375 Compressor compressor = "xml".equalsIgnoreCase(type) ? createXmlCompressor() : createHtmlCompressor();
376 Map<String, String> ioMap = buildInputOutputMap();
377 for (Map.Entry<String, String> entry : ioMap.entrySet()) {
378 writeResource(compressor.compress(readResource(buildReader(entry.getKey()))),
379 buildWriter(entry.getValue()));
380 }
381 }
382
383 } catch (NoClassDefFoundError e) {
384 if (HtmlCompressor.JS_COMPRESSOR_CLOSURE.equalsIgnoreCase(jsCompressorOpt)) {
385 logger.info("ERROR: For JavaScript compression using Google Closure Compiler\n"
386 + "additional jar file called compiler.jar must be present\n"
387 + "in the same directory as HtmlCompressor jar");
388 } else {
389 logger.info("ERROR: For CSS or JavaScript compression using YUICompressor additional jar file \n"
390 + "called yuicompressor.jar must be present\n" + "in the same directory as HtmlCompressor jar");
391 }
392 logger.trace("", e);
393 } catch (OptionException e) {
394 logger.info("{}", e.getMessage());
395 logger.trace("", e);
396 printUsage();
397 } catch (IOException | IllegalArgumentException e) {
398 logger.info("{}", e.getMessage());
399 logger.trace("", e);
400 }
401
402 }
403
404
405
406
407
408
409
410
411
412 private Compressor createHtmlCompressor() throws OptionException {
413
414 boolean useClosureCompressor = HtmlCompressor.JS_COMPRESSOR_CLOSURE.equalsIgnoreCase(jsCompressorOpt);
415
416
417 List<Pattern> preservePatterns = new ArrayList<>();
418
419
420 if (preservePhpTagsOpt) {
421 preservePatterns.add(HtmlCompressor.PHP_TAG_PATTERN);
422 }
423
424 if (preserveServerScriptTagsOpt) {
425 preservePatterns.add(HtmlCompressor.SERVER_SCRIPT_TAG_PATTERN);
426 }
427
428 if (preserveSsiTagsOpt) {
429 preservePatterns.add(HtmlCompressor.SERVER_SIDE_INCLUDE_PATTERN);
430 }
431
432 if (patternsFilenameOpt != null) {
433
434 try (FileInputStream stream = new FileInputStream(patternsFilenameOpt);
435 BufferedReader patternsIn = new BufferedReader(new InputStreamReader(stream, charsetOpt))) {
436
437 String line = null;
438 while ((line = patternsIn.readLine()) != null) {
439 if (line.length() > 0) {
440 try {
441 preservePatterns.add(Pattern.compile(line));
442 } catch (PatternSyntaxException e) {
443 logger.trace("", e);
444 throw new IllegalArgumentException(
445 "Regular expression compilation error: " + e.getMessage());
446 }
447 }
448 }
449 } catch (IOException e) {
450 logger.trace("", e);
451 throw new IllegalArgumentException("Unable to read custom pattern definitions file: " + e.getMessage());
452 }
453 }
454
455
456 HtmlCompressor htmlCompressor = new HtmlCompressor();
457
458 htmlCompressor.setRemoveComments(!preserveCommentsOpt);
459 htmlCompressor.setRemoveMultiSpaces(!preserveMultiSpacesOpt);
460 htmlCompressor.setRemoveIntertagSpaces(removeIntertagSpacesOpt);
461 htmlCompressor.setRemoveQuotes(removeQuotesOpt);
462 htmlCompressor.setPreserveLineBreaks(preserveLineBreaksOpt);
463 htmlCompressor.setCompressJavaScript(compressJsOpt);
464 htmlCompressor.setCompressCss(compressCssOpt);
465
466 htmlCompressor.setSimpleDoctype(simpleDoctypeOpt);
467 htmlCompressor.setRemoveScriptAttributes(removeScriptAttributesOpt);
468 htmlCompressor.setRemoveStyleAttributes(removeStyleAttributesOpt);
469 htmlCompressor.setRemoveLinkAttributes(removeLinkAttributesOpt);
470 htmlCompressor.setRemoveFormAttributes(removeFormAttributesOpt);
471 htmlCompressor.setRemoveInputAttributes(removeInputAttributesOpt);
472 htmlCompressor.setSimpleBooleanAttributes(simpleBooleanAttributesOpt);
473 htmlCompressor.setRemoveJavaScriptProtocol(removeJavaScriptProtocolOpt);
474 htmlCompressor.setRemoveHttpProtocol(removeHttpProtocolOpt);
475 htmlCompressor.setRemoveHttpsProtocol(removeHttpsProtocolOpt);
476 htmlCompressor.setRemoveSurroundingSpaces(removeSurroundingSpacesOpt);
477
478 htmlCompressor.setPreservePatterns(preservePatterns);
479
480 htmlCompressor.setYuiJsNoMunge(nomungeOpt);
481 htmlCompressor.setYuiJsPreserveAllSemiColons(preserveSemiOpt);
482 htmlCompressor.setYuiJsDisableOptimizations(disableOptimizationsOpt);
483 htmlCompressor.setYuiJsLineBreak(linebreakOpt);
484 htmlCompressor.setYuiCssLineBreak(linebreakOpt);
485
486
487 if (compressJsOpt && useClosureCompressor) {
488 ClosureJavaScriptCompressor closureCompressor = new ClosureJavaScriptCompressor();
489
490 if (closureOptLevelOpt.equalsIgnoreCase(ClosureJavaScriptCompressor.COMPILATION_LEVEL_ADVANCED)) {
491 closureCompressor.setCompilationLevel(CompilationLevel.ADVANCED_OPTIMIZATIONS);
492 closureCompressor.setCustomExternsOnly(closureCustomExternsOnlyOpt);
493
494
495 if (!closureExternsOpt.isEmpty()) {
496 List<SourceFile> externs = new ArrayList<>();
497 for (String externFile : closureExternsOpt) {
498 externs.add(SourceFile.fromFile(externFile));
499 }
500 closureCompressor.setExterns(externs);
501 }
502 } else if (closureOptLevelOpt.equalsIgnoreCase(ClosureJavaScriptCompressor.COMPILATION_LEVEL_WHITESPACE)) {
503 closureCompressor.setCompilationLevel(CompilationLevel.WHITESPACE_ONLY);
504 } else {
505 closureCompressor.setCompilationLevel(CompilationLevel.SIMPLE_OPTIMIZATIONS);
506 }
507
508 htmlCompressor.setJavaScriptCompressor(closureCompressor);
509 }
510
511 return htmlCompressor;
512 }
513
514
515
516
517
518
519
520
521
522
523
524 private Compressor createXmlCompressor() throws IllegalArgumentException, OptionException {
525 XmlCompressor xmlCompressor = new XmlCompressor();
526 xmlCompressor.setRemoveComments(!preserveCommentsOpt);
527 xmlCompressor.setRemoveIntertagSpaces(!preserveIntertagSpacesOpt);
528
529 return xmlCompressor;
530 }
531
532
533
534
535
536
537
538
539
540
541
542 private Map<String, String> buildInputOutputMap() throws IllegalArgumentException, IOException {
543 Map<String, String> map = new HashMap<>();
544
545 File outpuFile = null;
546 if (outputFilenameOpt != null) {
547 outpuFile = new File(outputFilenameOpt);
548
549
550 if (outputFilenameOpt.endsWith("/") || outputFilenameOpt.endsWith("\\")) {
551 outpuFile.mkdirs();
552 } else {
553 new File(outpuFile.getCanonicalFile().getParent()).mkdirs();
554 }
555 }
556
557 if (fileArgsOpt.size() > 1 && (outpuFile == null || !outpuFile.isDirectory())) {
558 throw new IllegalArgumentException("Output must be a directory and end with a slash (/)");
559 }
560
561 if (fileArgsOpt.isEmpty()) {
562 map.put(null, outputFilenameOpt);
563 } else {
564 for (int i = 0; i < fileArgsOpt.size(); i++) {
565 if (!urlPattern.matcher(fileArgsOpt.get(i)).matches()) {
566 File inputFile = new File(fileArgsOpt.get(i));
567 if (inputFile.isDirectory()) {
568
569 if (outpuFile != null && outpuFile.isDirectory()) {
570 if (!recursiveOpt) {
571
572 for (File file : inputFile
573 .listFiles(new CompressorFileFilter(typeOpt, filemaskOpt, false))) {
574 if (!file.isDirectory()) {
575 String from = file.getCanonicalPath();
576 String to = from.replaceFirst(escRegEx(inputFile.getCanonicalPath()),
577 Matcher.quoteReplacement(outpuFile.getCanonicalPath()));
578 map.put(from, to);
579 }
580 }
581 } else {
582
583 ArrayDeque<File> fileStack = new ArrayDeque<>();
584 fileStack.push(inputFile);
585 while (!fileStack.isEmpty()) {
586 File child = fileStack.pop();
587 if (child.isDirectory()) {
588 for (File f : child
589 .listFiles(new CompressorFileFilter(typeOpt, filemaskOpt, true))) {
590 fileStack.push(f);
591 }
592 } else if (child.isFile()) {
593 String from = child.getCanonicalPath();
594 String to = from.replaceFirst(escRegEx(inputFile.getCanonicalPath()),
595 Matcher.quoteReplacement(outpuFile.getCanonicalPath()));
596 map.put(from, to);
597
598 new File(new File(to).getCanonicalFile().getParent()).mkdirs();
599 }
600 }
601 }
602 } else {
603 throw new IllegalArgumentException("Output must be a directory and end with a slash (/)");
604 }
605 } else {
606
607 if (outpuFile != null && outpuFile.isDirectory()) {
608 String from = inputFile.getCanonicalPath();
609 String to = from.replaceFirst(
610 escRegEx(inputFile.getCanonicalFile().getParentFile().getCanonicalPath()),
611 Matcher.quoteReplacement(outpuFile.getCanonicalPath()));
612 map.put(fileArgsOpt.get(i), to);
613 } else {
614 map.put(fileArgsOpt.get(i), outputFilenameOpt);
615 }
616
617 }
618 } else {
619
620 if (fileArgsOpt.size() == 1 && (outpuFile == null || !outpuFile.isDirectory())) {
621 map.put(fileArgsOpt.get(i), outputFilenameOpt);
622 } else {
623 throw new IllegalArgumentException(
624 "Input URL should be single and cannot have directory as output");
625 }
626 }
627 }
628 }
629
630 return map;
631 }
632
633
634
635
636
637
638
639
640
641
642
643
644 private BufferedReader buildReader(String filename) throws IOException {
645 if (filename == null) {
646 return new BufferedReader(new InputStreamReader(System.in, charsetOpt));
647 } else if (urlPattern.matcher(filename).matches()) {
648 return new BufferedReader(
649 new InputStreamReader(new URL(filename).openConnection().getInputStream(), charsetOpt));
650 } else {
651 return new BufferedReader(new InputStreamReader(new FileInputStream(filename), charsetOpt));
652 }
653 }
654
655
656
657
658
659
660
661
662
663
664
665
666 private Writer buildWriter(String filename) throws IOException {
667 if (filename == null) {
668 return new OutputStreamWriter(System.out, charsetOpt);
669 } else {
670 return new OutputStreamWriter(new FileOutputStream(filename), charsetOpt);
671 }
672 }
673
674
675
676
677
678
679
680
681
682
683
684
685 private String readResource(BufferedReader input) throws IOException {
686 StringBuilder source = new StringBuilder();
687 try {
688 String line = null;
689 while ((line = input.readLine()) != null) {
690 source.append(line);
691 source.append(System.getProperty("line.separator"));
692 }
693 } finally {
694 closeStream(input);
695 }
696 return source.toString();
697 }
698
699
700
701
702
703
704
705
706
707
708
709
710 private void writeResource(String content, Writer output) throws IOException {
711 try {
712 output.write(content);
713 } finally {
714 closeStream(output);
715 }
716 }
717
718
719
720
721
722
723
724 private void closeStream(Closeable stream) {
725 if (stream != null) {
726 try {
727 stream.close();
728 } catch (IOException e) {
729 logger.trace("", e);
730 }
731 }
732 }
733
734
735
736
737
738
739
740
741
742 private String escRegEx(String inStr) {
743 return inStr.replaceAll("([\\\\*+\\[\\](){}\\$.?\\^|])", "\\\\$1");
744 }
745
746
747
748
749 private void printUsage() {
750 logger.info("Usage: java -jar htmlcompressor.jar [options] [input]\n\n"
751
752 + "[input] URL, filename, directory, or space separated list\n"
753 + " of files and directories to compress.\n"
754 + " If none provided reads from <stdin>\n\n"
755
756 + "Global Options:\n" + " -?, /?, -h, --help Displays this help screen\n"
757 + " -t, --type <html|xml> If not provided autodetects from file extension\n"
758 + " -r, --recursive Process files inside subdirectories\n"
759 + " -c, --charset <charset> Charset for reading files, UTF-8 by default\n"
760 + " -m, --mask <filemask> Filter input files inside directories by mask\n"
761 + " -o, --output <path> Filename or directory for compression results.\n"
762 + " If none provided outputs result to <stdout>\n"
763 + " -a, --analyze Tries different settings and displays report.\n"
764 + " All settings except --js-compressor are ignored\n\n"
765
766 + "XML Compression Options:\n" + " --preserve-comments Preserve comments\n"
767 + " --preserve-intertag-spaces Preserve intertag spaces\n\n"
768
769 + "HTML Compression Options:\n" + " --preserve-comments Preserve comments\n"
770 + " --preserve-multi-spaces Preserve multiple spaces\n"
771 + " --preserve-line-breaks Preserve line breaks\n"
772 + " --remove-intertag-spaces Remove intertag spaces\n"
773 + " --remove-quotes Remove unneeded quotes\n"
774 + " --simple-doctype Change doctype to <!DOCTYPE html>\n"
775 + " --remove-style-attr Remove TYPE attribute from STYLE tags\n"
776 + " --remove-link-attr Remove TYPE attribute from LINK tags\n"
777 + " --remove-script-attr Remove TYPE and LANGUAGE from SCRIPT tags\n"
778 + " --remove-form-attr Remove METHOD=\"GET\" from FORM tags\n"
779 + " --remove-input-attr Remove TYPE=\"TEXT\" from INPUT tags\n"
780 + " --simple-bool-attr Remove values from boolean tag attributes\n"
781 + " --remove-js-protocol Remove \"javascript:\" from inline event handlers\n"
782 + " --remove-http-protocol Remove \"http:\" from tag attributes\n"
783 + " --remove-https-protocol Remove \"https:\" from tag attributes\n"
784 + " --remove-surrounding-spaces <min|max|all|custom_list>\n"
785 + " Predefined or custom comma separated list of tags\n"
786 + " --compress-js Enable inline JavaScript compression\n"
787 + " --compress-css Enable inline CSS compression using YUICompressor\n"
788 + " --js-compressor <yui|closure> Switch inline JavaScript compressor between\n"
789 + " YUICompressor (default) and Closure Compiler\n\n"
790
791 + "JavaScript Compression Options for YUI Compressor:\n"
792 + " --nomunge Minify only, do not obfuscate\n"
793 + " --preserve-semi Preserve all semicolons\n"
794 + " --disable-optimizations Disable all micro optimizations\n"
795 + " --line-break <column num> Insert a line break after the specified column\n\n"
796
797 + "JavaScript Compression Options for Google Closure Compiler:\n"
798 + " --closure-opt-level <simple|advanced|whitespace>\n"
799 + " Sets level of optimization (simple by default)\n"
800 + " --closure-externs <file> Sets custom externs file, repeat for each file\n"
801 + " --closure-custom-externs-only Disable default built-in externs\n\n"
802
803 + "CSS Compression Options for YUI Compressor:\n"
804 + " --line-break <column num> Insert a line break after the specified column\n\n"
805
806 + "Custom Block Preservation Options:\n" + " --preserve-php Preserve <?php ... ?> tags\n"
807 + " --preserve-server-script Preserve <% ... %> tags\n"
808 + " --preserve-ssi Preserve <!--# ... --> tags\n"
809 + " -p, --preserve <path> Read regular expressions that define\n"
810 + " custom preservation rules from a file\n\n"
811
812 + "Please note that if you enable CSS or JavaScript compression, additional\n"
813 + "YUI Compressor or Google Closure Compiler jar files must be present\n"
814 + "in the same directory as this jar."
815
816 );
817 }
818
819
820
821
822 private class CompressorFileFilter implements FileFilter {
823
824
825 private Pattern filemaskPattern;
826
827
828 private boolean withDirs;
829
830
831
832
833
834
835
836
837
838
839
840 public CompressorFileFilter(String type, String filemask, boolean withDirs) {
841
842 this.withDirs = withDirs;
843
844 if (filemask == null) {
845 if (type != null && "xml".equalsIgnoreCase(type)) {
846 filemaskPattern = Pattern.compile("^.*\\.xml$", Pattern.CASE_INSENSITIVE);
847 } else {
848 filemaskPattern = Pattern.compile("^.*\\.html?$", Pattern.CASE_INSENSITIVE);
849 }
850 } else {
851
852 filemask = filemask.replaceAll(escRegEx("."), Matcher.quoteReplacement("\\."));
853 filemask = filemask.replaceAll(escRegEx("*"), Matcher.quoteReplacement(".*"));
854 filemask = filemask.replaceAll(escRegEx("?"), Matcher.quoteReplacement("."));
855 filemask = filemask.replaceAll(escRegEx(";"), Matcher.quoteReplacement("$|^"));
856 filemask = "^" + filemask + "$";
857
858 filemaskPattern = Pattern.compile(filemask, Pattern.CASE_INSENSITIVE);
859 }
860 }
861
862 @Override
863 public boolean accept(File file) {
864 if (!withDirs) {
865
866 if (!file.isDirectory()) {
867 return filemaskPattern.matcher(file.getName()).matches();
868 }
869 } else {
870
871 return file.isDirectory() || filemaskPattern.matcher(file.getName()).matches();
872 }
873 return false;
874 }
875
876 }
877
878 }