• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hazendaz / htmlcompressor-maven-plugin / 75

20 Apr 2025 06:38PM UTC coverage: 36.479% (+0.2%) from 36.249%
75

push

github

hazendaz
Change %s injector to be quoted "%s" instead

the underlying code with %s is invalid javascript.  By doing it this way, its now compliant.  The result is the same.  This is a breaking change for those using this.

44 of 218 branches covered (20.18%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

133 existing lines in 5 files now uncovered.

273 of 651 relevant lines covered (41.94%)

0.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

39.24
/src/main/java/com/tunyk/mvn/plugins/htmlcompressor/HtmlCompressorMojo.java
1
/*
2
 * Copyright (c) 2011-2025 Alex Tunyk <alex at tunyk.com>.
3
 * Copyright (c) 2011-2025 Hazendaz <github.com/hazendaz>.
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 *   https://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 *
17
 * See the NOTICE file distributed with this work for additional information
18
 * regarding copyright ownership.
19
 */
20
package com.tunyk.mvn.plugins.htmlcompressor;
21

22
import com.google.javascript.jscomp.CompilationLevel;
23
import com.google.javascript.jscomp.SourceFile;
24
import com.googlecode.htmlcompressor.compressor.ClosureJavaScriptCompressor;
25
import com.googlecode.htmlcompressor.compressor.Compressor;
26

27
import java.io.File;
28
import java.io.IOException;
29
import java.nio.charset.Charset;
30
import java.nio.file.Files;
31
import java.nio.file.Path;
32
import java.text.DecimalFormat;
33
import java.text.NumberFormat;
34
import java.util.ArrayList;
35
import java.util.List;
36
import java.util.regex.Pattern;
37
import java.util.regex.PatternSyntaxException;
38

39
import org.apache.maven.plugin.AbstractMojo;
40
import org.apache.maven.plugin.MojoExecutionException;
41
import org.apache.maven.plugins.annotations.LifecyclePhase;
42
import org.apache.maven.plugins.annotations.Mojo;
43
import org.apache.maven.plugins.annotations.Parameter;
44

45
/**
46
 * Compress HTML files.
47
 */
48
@Mojo(name = "html", defaultPhase = LifecyclePhase.COMPILE, requiresProject = false, threadSafe = true)
49
public class HtmlCompressorMojo extends AbstractMojo {
1✔
50

51
    /** file where statistics of html compression is stored. */
52
    @Parameter(property = "htmlcompressor.htmlCompressionStatistics", defaultValue = "${project.build.directory}/htmlcompressor/html-compression-statistics.txt")
1✔
53
    private String htmlCompressionStatistics = "target/htmlcompressor/html-compression-statistics.txt";
54

55
    /**
56
     * file types to be processed.
57
     *
58
     * @deprecated use fileExtensions
59
     */
60
    @Deprecated
61
    @Parameter(property = "htmlcompressor.fileExt")
62
    private String[] fileExt;
63

64
    /**
65
     * File extensions to be processed.
66
     */
67
    @Parameter(property = "htmlcompressor.fileExtensions")
68
    private String[] fileExtensions;
69

70
    /** if false all compression is off (default is true). */
71
    @Parameter(property = "htmlcompressor.enabled", defaultValue = "true")
1✔
72
    private boolean enabled = true;
73

74
    /** Skip run of plugin. */
75
    @Parameter(defaultValue = "false", alias = "skip", property = "skip")
76
    private boolean skip;
77

78
    /** if false keeps HTML comments (default is true). */
79
    @Parameter(property = "htmlcompressor.removeComments", defaultValue = "true")
1✔
80
    private boolean removeComments = true;
81

82
    /** if false keeps multiple whitespace characters (default is true). */
83
    @Parameter(property = "htmlcompressor.removeMultiSpaces", defaultValue = "true")
1✔
84
    private boolean removeMultiSpaces = true;
85

86
    /** removes iter-tag whitespace characters. */
87
    @Parameter(property = "htmlcompressor.removeIntertagSpaces", defaultValue = "false")
88
    private boolean removeIntertagSpaces;
89

90
    /** removes unnecessary tag attribute quotes. */
91
    @Parameter(property = "htmlcompressor.removeQuotes", defaultValue = "false")
92
    private boolean removeQuotes;
93

94
    /** simplify existing doctype. */
95
    @Parameter(property = "htmlcompressor.simpleDoctype", defaultValue = "false")
96
    private boolean simpleDoctype;
97

98
    /** remove optional attributes from script tags. */
99
    @Parameter(property = "htmlcompressor.removeScriptAttributes", defaultValue = "false")
100
    private boolean removeScriptAttributes;
101

102
    /** remove optional attributes from style tags. */
103
    @Parameter(property = "htmlcompressor.removeStyleAttributes", defaultValue = "false")
104
    private boolean removeStyleAttributes;
105

106
    /** remove optional attributes from link tags. */
107
    @Parameter(property = "htmlcompressor.removeLinkAttributes", defaultValue = "false")
108
    private boolean removeLinkAttributes;
109

110
    /** remove optional attributes from form tags. */
111
    @Parameter(property = "htmlcompressor.removeFormAttributes", defaultValue = "false")
112
    private boolean removeFormAttributes;
113

114
    /** remove optional attributes from input tags. */
115
    @Parameter(property = "htmlcompressor.removeInputAttributes", defaultValue = "false")
116
    private boolean removeInputAttributes;
117

118
    /** remove values from boolean tag attributes. */
119
    @Parameter(property = "htmlcompressor.simpleBooleanAttributes", defaultValue = "false")
120
    private boolean simpleBooleanAttributes;
121

122
    /** remove "javascript:" from inline event handlers. */
123
    @Parameter(property = "htmlcompressor.removeJavaScriptProtocol", defaultValue = "false")
124
    private boolean removeJavaScriptProtocol;
125

126
    /** replace "http://" with "//" inside tag attributes. */
127
    @Parameter(property = "htmlcompressor.removeHttpProtocol", defaultValue = "false")
128
    private boolean removeHttpProtocol;
129

130
    /** replace "https://" with "//" inside tag attributes. */
131
    @Parameter(property = "htmlcompressor.removeHttpsProtocol", defaultValue = "false")
132
    private boolean removeHttpsProtocol;
133

134
    /** compress inline css. */
135
    @Parameter(property = "htmlcompressor.compressCss", defaultValue = "false")
136
    private boolean compressCss;
137

138
    /** preserves original line breaks. */
139
    @Parameter(property = "htmlcompressor.preserveLineBreaks", defaultValue = "false")
140
    private boolean preserveLineBreaks;
141

142
    /** --line-break param for Yahoo YUI Compressor. */
143
    @Parameter(property = "htmlcompressor.yuiCssLineBreak", defaultValue = "-1")
1✔
144
    private int yuiCssLineBreak = -1;
145

146
    /** css compressor. */
147
    // TODO JWL 4/22/2023 Unsupported
148
    @SuppressWarnings("unused")
149
    @Parameter(property = "htmlcompressor.cssCompressor", defaultValue = "")
150
    private Compressor cssCompressor;
151

152
    /** compress inline javascript. */
153
    @Parameter(property = "htmlcompressor.compressJavaScript", defaultValue = "false")
154
    private boolean compressJavaScript;
155

156
    /** javascript compression: "yui" or "closure". */
157
    @Parameter(property = "htmlcompressor.jsCompressor", defaultValue = "yui")
1✔
158
    private String jsCompressor = "yui";
159

160
    /** javascript compression. */
161
    // TODO JWL 4/22/2023Unsupported
162
    @SuppressWarnings("unused")
163
    @Parameter(property = "htmlcompressor.javaScriptCompressor", defaultValue = "")
164
    private Compressor javaScriptCompressor;
165

166
    /** --nomunge param for Yahoo YUI Compressor. */
167
    @Parameter(property = "htmlcompressor.yuiJsNoMunge", defaultValue = "false")
168
    private boolean yuiJsNoMunge;
169

170
    /** --preserve-semi param for Yahoo YUI Compressor. */
171
    @Parameter(property = "htmlcompressor.yuiJsPreserveAllSemiColons", defaultValue = "false")
172
    private boolean yuiJsPreserveAllSemiColons;
173

174
    /** --line-break param for Yahoo YUI Compressor. */
175
    @Parameter(property = "htmlcompressor.yuiJsLineBreak", defaultValue = "-1")
1✔
176
    private int yuiJsLineBreak = -1;
177

178
    /** closureOptLevel = "simple", "advanced" or "whitespace". */
179
    @Parameter(property = "htmlcompressor.closureOptLevel", defaultValue = "simple")
1✔
180
    private String closureOptLevel = "simple";
181

182
    /** --disable-optimizations param for Yahoo YUI Compressor. */
183
    @Parameter(property = "htmlcompressor.yuiJsDisableOptimizations", defaultValue = "false")
184
    private boolean yuiJsDisableOptimizations;
185

186
    /**
187
     * predefined patterns for most often used custom preservation rules: PHP_TAG_PATTERN and SERVER_SCRIPT_TAG_PATTERN.
188
     */
189
    @Parameter(property = "htmlcompressor.predefinedPreservePatterns")
190
    private String[] predefinedPreservePatterns;
191

192
    /** preserve patterns. */
193
    @Parameter(property = "htmlcompressor.preservePatterns")
194
    private String[] preservePatterns;
195

196
    /** list of files containing preserve patterns. */
197
    @Parameter(property = "htmlcompressor.preservePatternFiles")
198
    private File[] preservePatternFiles;
199

200
    /** HTML compression statistics. */
201
    @Parameter(property = "htmlcompressor.generateStatistics", defaultValue = "true")
1✔
202
    private boolean generateStatistics = true;
203

204
    /**
205
     * source folder where html files are located.
206
     */
207
    @Parameter(property = "htmlcompressor.srcFolder", defaultValue = "${basedir}/src/main/resources")
1✔
208
    private String srcFolder = "src/main/resources";
209

210
    /**
211
     * target folder where compressed html files will be placed.
212
     */
213
    @Parameter(property = "htmlcompressor.targetFolder", defaultValue = "${project.build.directory}/classes")
1✔
214
    private String targetFolder = "target/classes";
215

216
    /**
217
     * Create javascript file which includes all compressed html files as json object. If set to true then
218
     * javascriptHtmlSpriteIntegrationFile param is required, otherwise it will throw exception.
219
     */
220
    @Parameter(property = "htmlcompressor.javascriptHtmlSprite", defaultValue = "true")
1✔
221
    private boolean javascriptHtmlSprite = true;
222

223
    /**
224
     * JavaScript sprite integration file (first occurrence of "%s" will be substituted by json with all compressed html
225
     * strings).
226
     */
227
    @Parameter(property = "htmlcompressor.javascriptHtmlSpriteIntegrationFile", defaultValue = "${basedir}/src/main/resources/html/integration.js")
1✔
228
    private String javascriptHtmlSpriteIntegrationFile = "src/main/resources/html/integration.js";
229

230
    /**
231
     * The target JavaScript sprite file with compressed html files as json object.
232
     */
233
    @Parameter(property = "htmlcompressor.javascriptHtmlSpriteTargetFile", defaultValue = "${project.build.directory}/htmlcompressor/html/integration.js")
1✔
234
    private String javascriptHtmlSpriteTargetFile = "target/htmlcompressor/html/integration.js";
235

236
    /** Charset encoding for files to read and create. */
237
    @Parameter(property = "htmlcompressor.encoding", defaultValue = "UTF-8")
1✔
238
    private String encoding = "UTF-8";
239

240
    /**
241
     * Disable default built-in closure externs.
242
     */
243
    @Parameter(property = "htmlcompressor.closureCustomExternsOnly", defaultValue = "false")
244
    private boolean closureCustomExternsOnly;
245

246
    /**
247
     * Sets custom closure externs file list.
248
     */
249
    @Parameter(property = "htmlcompressor.closureExterns")
250
    private String[] closureExterns;
251

252
    @Override
253
    public void execute() throws MojoExecutionException {
254
        // Check if plugin run should be skipped
255
        if (this.skip) {
1!
256
            getLog().info("HtmlCompressor is skipped");
×
UNCOV
257
            return;
×
258
        }
259

260
        if (!enabled) {
1!
261
            getLog().info("HTML compression was turned off.");
×
UNCOV
262
            return;
×
263
        }
264

265
        if (!Files.exists(Path.of(srcFolder))) {
1!
266
            getLog().warn("Compressor folder does not exist, skipping compression of " + srcFolder);
×
UNCOV
267
            return;
×
268
        }
269

270
        getLog().info("Compressing " + srcFolder);
1✔
271
        HtmlCompressor htmlCompressor = new HtmlCompressor(srcFolder, targetFolder);
1✔
272

273
        // Deprecated
274
        if (fileExt != null && fileExtensions == null) {
1!
UNCOV
275
            fileExtensions = fileExt;
×
276
        }
277

278
        htmlCompressor.setFileExtensions(fileExtensions);
1✔
279
        htmlCompressor.setFileEncoding(Charset.forName(encoding));
1✔
280
        htmlCompressor.setCreateJsonFile(javascriptHtmlSprite);
1✔
281
        htmlCompressor.setJsonIntegrationFilePath(javascriptHtmlSpriteIntegrationFile);
1✔
282
        htmlCompressor.setTargetJsonFilePath(javascriptHtmlSpriteTargetFile);
1✔
283

284
        com.googlecode.htmlcompressor.compressor.HtmlCompressor htmlCompressorHandler = new com.googlecode.htmlcompressor.compressor.HtmlCompressor();
1✔
285
        htmlCompressorHandler.setEnabled(enabled);
1✔
286
        htmlCompressorHandler.setRemoveComments(removeComments);
1✔
287
        htmlCompressorHandler.setRemoveMultiSpaces(removeMultiSpaces);
1✔
288
        htmlCompressorHandler.setRemoveIntertagSpaces(removeIntertagSpaces);
1✔
289
        htmlCompressorHandler.setRemoveQuotes(removeQuotes);
1✔
290
        htmlCompressorHandler.setSimpleDoctype(simpleDoctype);
1✔
291
        htmlCompressorHandler.setRemoveScriptAttributes(removeScriptAttributes);
1✔
292
        htmlCompressorHandler.setRemoveStyleAttributes(removeStyleAttributes);
1✔
293
        htmlCompressorHandler.setRemoveLinkAttributes(removeLinkAttributes);
1✔
294
        htmlCompressorHandler.setRemoveFormAttributes(removeFormAttributes);
1✔
295
        htmlCompressorHandler.setRemoveInputAttributes(removeInputAttributes);
1✔
296
        htmlCompressorHandler.setSimpleBooleanAttributes(simpleBooleanAttributes);
1✔
297
        htmlCompressorHandler.setRemoveJavaScriptProtocol(removeJavaScriptProtocol);
1✔
298
        htmlCompressorHandler.setRemoveHttpProtocol(removeHttpProtocol);
1✔
299
        htmlCompressorHandler.setRemoveHttpsProtocol(removeHttpsProtocol);
1✔
300
        htmlCompressorHandler.setCompressCss(compressCss);
1✔
301
        htmlCompressorHandler.setPreserveLineBreaks(preserveLineBreaks);
1✔
302
        htmlCompressorHandler.setYuiCssLineBreak(yuiCssLineBreak);
1✔
303
        htmlCompressorHandler.setCompressJavaScript(compressJavaScript);
1✔
304
        htmlCompressorHandler.setYuiJsNoMunge(yuiJsNoMunge);
1✔
305
        htmlCompressorHandler.setYuiJsPreserveAllSemiColons(yuiJsPreserveAllSemiColons);
1✔
306
        htmlCompressorHandler.setYuiJsLineBreak(yuiJsLineBreak);
1✔
307
        htmlCompressorHandler.setYuiJsDisableOptimizations(yuiJsDisableOptimizations);
1✔
308
        htmlCompressorHandler.setGenerateStatistics(generateStatistics);
1✔
309

310
        if (jsCompressor.equalsIgnoreCase("closure")) {
1!
311
            ClosureJavaScriptCompressor closureCompressor = new ClosureJavaScriptCompressor();
×
312
            if (closureOptLevel != null
×
313
                    && closureOptLevel.equalsIgnoreCase(ClosureJavaScriptCompressor.COMPILATION_LEVEL_ADVANCED)) {
×
314
                closureCompressor.setCompilationLevel(CompilationLevel.ADVANCED_OPTIMIZATIONS);
×
315
                closureCompressor.setCustomExternsOnly(closureCustomExternsOnly);
×
316
                if (closureExterns.length > 0) {
×
317
                    List<SourceFile> externs = new ArrayList<>();
×
318
                    for (String externFile : closureExterns) {
×
UNCOV
319
                        externs.add(SourceFile.fromFile(externFile));
×
320
                    }
321
                    closureCompressor.setExterns(externs);
×
322
                }
×
323
            } else if (closureOptLevel != null
×
324
                    && closureOptLevel.equalsIgnoreCase(ClosureJavaScriptCompressor.COMPILATION_LEVEL_WHITESPACE)) {
×
UNCOV
325
                closureCompressor.setCompilationLevel(CompilationLevel.WHITESPACE_ONLY);
×
326
            } else {
UNCOV
327
                closureCompressor.setCompilationLevel(CompilationLevel.SIMPLE_OPTIMIZATIONS);
×
328
            }
329

UNCOV
330
            htmlCompressorHandler.setJavaScriptCompressor(closureCompressor);
×
331
        }
332

333
        List<Pattern> preservePatternList = new ArrayList<>();
1✔
334
        boolean phpTagPatternAdded = false;
1✔
335
        boolean serverScriptTagPatternAdded = false;
1✔
336
        if (predefinedPreservePatterns != null) {
1!
337
            for (String pattern : predefinedPreservePatterns) {
×
338
                if (!phpTagPatternAdded && pattern.equalsIgnoreCase("PHP_TAG_PATTERN")) {
×
339
                    preservePatternList.add(com.googlecode.htmlcompressor.compressor.HtmlCompressor.PHP_TAG_PATTERN);
×
340
                    phpTagPatternAdded = true;
×
341
                } else if (!serverScriptTagPatternAdded && pattern.equalsIgnoreCase("SERVER_SCRIPT_TAG_PATTERN")) {
×
342
                    preservePatternList
×
343
                            .add(com.googlecode.htmlcompressor.compressor.HtmlCompressor.SERVER_SCRIPT_TAG_PATTERN);
×
UNCOV
344
                    serverScriptTagPatternAdded = true;
×
345
                }
346
            }
347
        }
348
        if (preservePatterns != null) {
1!
349
            for (String preservePatternString : preservePatterns) {
×
UNCOV
350
                if (!preservePatternString.isEmpty()) {
×
351
                    try {
352
                        preservePatternList.add(Pattern.compile(preservePatternString));
×
353
                    } catch (PatternSyntaxException e) {
×
354
                        throw new MojoExecutionException(e.getMessage());
×
UNCOV
355
                    }
×
356
                }
357
            }
358
        }
359
        if (preservePatternFiles != null) {
1!
UNCOV
360
            for (File file : preservePatternFiles) {
×
361
                try {
362
                    List<String> fileLines = Files.readAllLines(file.toPath(), Charset.forName(encoding));
×
363
                    for (String line : fileLines) {
×
364
                        if (!line.isEmpty()) {
×
UNCOV
365
                            preservePatternList.add(Pattern.compile(line));
×
366
                        }
367
                    }
×
368
                } catch (IOException | PatternSyntaxException e) {
×
369
                    throw new MojoExecutionException(e.getMessage());
×
UNCOV
370
                }
×
371
            }
372
        }
373
        htmlCompressorHandler.setPreservePatterns(preservePatternList);
1✔
374
        htmlCompressor.setHtmlCompressor(htmlCompressorHandler);
1✔
375

376
        try {
377
            htmlCompressor.compress();
1✔
378
        } catch (IOException e) {
×
UNCOV
379
            throw new MojoExecutionException(e.getMessage());
×
380
        }
1✔
381

382
        // The system of units (SI) as 1000 rather than 1024
383
        boolean systemOfUnits = true;
1✔
384

385
        int origFilesizeBytes = -1;
1✔
386
        try {
387
            origFilesizeBytes = htmlCompressor.getHtmlCompressor().getStatistics().getOriginalMetrics().getFilesize();
1✔
388
        } catch (NullPointerException e) {
×
389
            getLog().info("No files found to compress, HTML compression completed.");
×
UNCOV
390
            return;
×
391
        }
1✔
392

393
        String origFilesize = FileTool.humanReadableByteCount(origFilesizeBytes, systemOfUnits);
1✔
394
        String origEmptyChars = String
1✔
395
                .valueOf(htmlCompressor.getHtmlCompressor().getStatistics().getOriginalMetrics().getEmptyChars());
1✔
396
        String origInlineEventSize = FileTool.humanReadableByteCount(
1✔
397
                htmlCompressor.getHtmlCompressor().getStatistics().getOriginalMetrics().getInlineEventSize(),
1✔
398
                systemOfUnits);
399
        String origInlineScriptSize = FileTool.humanReadableByteCount(
1✔
400
                htmlCompressor.getHtmlCompressor().getStatistics().getOriginalMetrics().getInlineScriptSize(),
1✔
401
                systemOfUnits);
402
        String origInlineStyleSize = FileTool.humanReadableByteCount(
1✔
403
                htmlCompressor.getHtmlCompressor().getStatistics().getOriginalMetrics().getInlineStyleSize(),
1✔
404
                systemOfUnits);
405

406
        int compFilesizeBytes = htmlCompressor.getHtmlCompressor().getStatistics().getCompressedMetrics().getFilesize();
1✔
407
        String compFilesize = FileTool.humanReadableByteCount(compFilesizeBytes, systemOfUnits);
1✔
408
        String compEmptyChars = String
1✔
409
                .valueOf(htmlCompressor.getHtmlCompressor().getStatistics().getCompressedMetrics().getEmptyChars());
1✔
410
        String compInlineEventSize = FileTool.humanReadableByteCount(
1✔
411
                htmlCompressor.getHtmlCompressor().getStatistics().getCompressedMetrics().getInlineEventSize(),
1✔
412
                systemOfUnits);
413
        String compInlineScriptSize = FileTool.humanReadableByteCount(
1✔
414
                htmlCompressor.getHtmlCompressor().getStatistics().getCompressedMetrics().getInlineScriptSize(),
1✔
415
                systemOfUnits);
416
        String compInlineStyleSize = FileTool.humanReadableByteCount(
1✔
417
                htmlCompressor.getHtmlCompressor().getStatistics().getCompressedMetrics().getInlineStyleSize(),
1✔
418
                systemOfUnits);
419

420
        String elapsedTime = FileTool.getElapsedHMSTime(htmlCompressor.getHtmlCompressor().getStatistics().getTime());
1✔
421
        String preservedSize = FileTool.humanReadableByteCount(
1✔
422
                htmlCompressor.getHtmlCompressor().getStatistics().getPreservedSize(), systemOfUnits);
1✔
423
        float compressionRatio = Float.valueOf(compFilesizeBytes) / Float.valueOf(origFilesizeBytes);
1✔
424
        float spaceSavings = Float.valueOf(1) - compressionRatio;
1✔
425

426
        String format = "%-30s%-30s%-30s%-2s";
1✔
427
        NumberFormat formatter = new DecimalFormat("#0.00");
1✔
428
        String eol = "\n";
1✔
429
        String hr = "+-----------------------------+-----------------------------+-----------------------------+";
1✔
430
        StringBuilder sb = new StringBuilder("HTML compression statistics:").append(eol);
1✔
431
        sb.append(hr).append(eol);
1✔
432
        sb.append(String.format(format, "| Category", "| Original", "| Compressed", "|")).append(eol);
1✔
433
        sb.append(hr).append(eol);
1✔
434
        sb.append(String.format(format, "| Filesize", "| " + origFilesize, "| " + compFilesize, "|")).append(eol);
1✔
435
        sb.append(String.format(format, "| Empty Chars", "| " + origEmptyChars, "| " + compEmptyChars, "|"))
1✔
436
                .append(eol);
1✔
437
        sb.append(String.format(format, "| Script Size", "| " + origInlineScriptSize, "| " + compInlineScriptSize, "|"))
1✔
438
                .append(eol);
1✔
439
        sb.append(String.format(format, "| Style Size", "| " + origInlineStyleSize, "| " + compInlineStyleSize, "|"))
1✔
440
                .append(eol);
1✔
441
        sb.append(String.format(format, "| Event Handler Size", "| " + origInlineEventSize, "| " + compInlineEventSize,
1✔
442
                "|")).append(eol);
1✔
443
        sb.append(hr).append(eol);
1✔
444
        sb.append(String.format("%-90s%-2s",
1✔
445
                String.format("| Time: %s, Preserved: %s, Compression Ratio: %s, Savings: %s%%", elapsedTime,
1✔
446
                        preservedSize, formatter.format(compressionRatio), formatter.format(spaceSavings * 100)),
1✔
447
                "|")).append(eol);
1✔
448
        sb.append(hr).append(eol);
1✔
449

450
        String statistics = sb.toString();
1✔
451
        getLog().info(statistics);
1✔
452
        try {
453
            Files.createDirectories(Path.of(htmlCompressionStatistics).getParent());
1✔
454
            Files.writeString(Path.of(htmlCompressionStatistics), statistics, Charset.forName(encoding));
1✔
455
        } catch (IOException e) {
×
UNCOV
456
            throw new MojoExecutionException(e.getMessage());
×
457
        }
1✔
458

459
        getLog().info("HTML compression completed.");
1✔
460
    }
1✔
461

462
    /**
463
     * Gets the html compression statistics.
464
     *
465
     * @return the html compression statistics
466
     */
467
    public String getHtmlCompressionStatistics() {
UNCOV
468
        return htmlCompressionStatistics;
×
469
    }
470

471
    /**
472
     * Sets the html compression statistics.
473
     *
474
     * @param htmlCompressionStatistics
475
     *            the new html compression statistics
476
     */
477
    public void setHtmlCompressionStatistics(String htmlCompressionStatistics) {
478
        this.htmlCompressionStatistics = htmlCompressionStatistics;
×
UNCOV
479
    }
×
480

481
    /**
482
     * Gets the file ext.
483
     *
484
     * @return the file ext
485
     *
486
     * @deprecated use getFileExtensions
487
     */
488
    @Deprecated
489
    public String[] getFileExt() {
UNCOV
490
        return fileExt;
×
491
    }
492

493
    /**
494
     * Sets the file ext.
495
     *
496
     * @param fileExt
497
     *            the new file ext
498
     *
499
     * @deprecated use setFileExtensions
500
     */
501
    @Deprecated
502
    public void setFileExt(String[] fileExt) {
503
        this.fileExt = fileExt;
×
UNCOV
504
    }
×
505

506
    /**
507
     * Gets the file ext.
508
     *
509
     * @return the file extensions
510
     */
511
    public String[] getFileExtensions() {
UNCOV
512
        return fileExtensions;
×
513
    }
514

515
    /**
516
     * Sets the file ext.
517
     *
518
     * @param fileExtensions
519
     *            the new file ext
520
     */
521
    public void setFileExtensions(String[] fileExtensions) {
522
        this.fileExtensions = fileExtensions;
×
UNCOV
523
    }
×
524

525
    /**
526
     * Gets the enabled.
527
     *
528
     * @return the enabled
529
     */
530
    public Boolean getEnabled() {
UNCOV
531
        return enabled;
×
532
    }
533

534
    /**
535
     * Sets the enabled.
536
     *
537
     * @param enabled
538
     *            the new enabled
539
     */
540
    public void setEnabled(Boolean enabled) {
541
        this.enabled = enabled;
×
UNCOV
542
    }
×
543

544
    /**
545
     * Gets the removes the comments.
546
     *
547
     * @return the removes the comments
548
     */
549
    public Boolean getRemoveComments() {
UNCOV
550
        return removeComments;
×
551
    }
552

553
    /**
554
     * Sets the removes the comments.
555
     *
556
     * @param removeComments
557
     *            the new removes the comments
558
     */
559
    public void setRemoveComments(Boolean removeComments) {
560
        this.removeComments = removeComments;
×
UNCOV
561
    }
×
562

563
    /**
564
     * Gets the removes the multi spaces.
565
     *
566
     * @return the removes the multi spaces
567
     */
568
    public Boolean getRemoveMultiSpaces() {
UNCOV
569
        return removeMultiSpaces;
×
570
    }
571

572
    /**
573
     * Sets the removes the multi spaces.
574
     *
575
     * @param removeMultiSpaces
576
     *            the new removes the multi spaces
577
     */
578
    public void setRemoveMultiSpaces(Boolean removeMultiSpaces) {
579
        this.removeMultiSpaces = removeMultiSpaces;
×
UNCOV
580
    }
×
581

582
    /**
583
     * Gets the removes the intertag spaces.
584
     *
585
     * @return the removes the intertag spaces
586
     */
587
    public Boolean getRemoveIntertagSpaces() {
UNCOV
588
        return removeIntertagSpaces;
×
589
    }
590

591
    /**
592
     * Sets the removes the intertag spaces.
593
     *
594
     * @param removeIntertagSpaces
595
     *            the new removes the intertag spaces
596
     */
597
    public void setRemoveIntertagSpaces(Boolean removeIntertagSpaces) {
598
        this.removeIntertagSpaces = removeIntertagSpaces;
×
UNCOV
599
    }
×
600

601
    /**
602
     * Gets the removes the quotes.
603
     *
604
     * @return the removes the quotes
605
     */
606
    public Boolean getRemoveQuotes() {
UNCOV
607
        return removeQuotes;
×
608
    }
609

610
    /**
611
     * Sets the removes the quotes.
612
     *
613
     * @param removeQuotes
614
     *            the new removes the quotes
615
     */
616
    public void setRemoveQuotes(Boolean removeQuotes) {
617
        this.removeQuotes = removeQuotes;
×
UNCOV
618
    }
×
619

620
    /**
621
     * Gets the simple doctype.
622
     *
623
     * @return the simple doctype
624
     */
625
    public Boolean getSimpleDoctype() {
UNCOV
626
        return simpleDoctype;
×
627
    }
628

629
    /**
630
     * Sets the simple doctype.
631
     *
632
     * @param simpleDoctype
633
     *            the new simple doctype
634
     */
635
    public void setSimpleDoctype(Boolean simpleDoctype) {
636
        this.simpleDoctype = simpleDoctype;
×
UNCOV
637
    }
×
638

639
    /**
640
     * Gets the removes the script attributes.
641
     *
642
     * @return the removes the script attributes
643
     */
644
    public Boolean getRemoveScriptAttributes() {
UNCOV
645
        return removeScriptAttributes;
×
646
    }
647

648
    /**
649
     * Sets the removes the script attributes.
650
     *
651
     * @param removeScriptAttributes
652
     *            the new removes the script attributes
653
     */
654
    public void setRemoveScriptAttributes(Boolean removeScriptAttributes) {
655
        this.removeScriptAttributes = removeScriptAttributes;
×
UNCOV
656
    }
×
657

658
    /**
659
     * Gets the removes the style attributes.
660
     *
661
     * @return the removes the style attributes
662
     */
663
    public Boolean getRemoveStyleAttributes() {
UNCOV
664
        return removeStyleAttributes;
×
665
    }
666

667
    /**
668
     * Sets the removes the style attributes.
669
     *
670
     * @param removeStyleAttributes
671
     *            the new removes the style attributes
672
     */
673
    public void setRemoveStyleAttributes(Boolean removeStyleAttributes) {
674
        this.removeStyleAttributes = removeStyleAttributes;
×
UNCOV
675
    }
×
676

677
    /**
678
     * Gets the removes the link attributes.
679
     *
680
     * @return the removes the link attributes
681
     */
682
    public Boolean getRemoveLinkAttributes() {
UNCOV
683
        return removeLinkAttributes;
×
684
    }
685

686
    /**
687
     * Sets the removes the link attributes.
688
     *
689
     * @param removeLinkAttributes
690
     *            the new removes the link attributes
691
     */
692
    public void setRemoveLinkAttributes(Boolean removeLinkAttributes) {
693
        this.removeLinkAttributes = removeLinkAttributes;
×
UNCOV
694
    }
×
695

696
    /**
697
     * Gets the removes the form attributes.
698
     *
699
     * @return the removes the form attributes
700
     */
701
    public Boolean getRemoveFormAttributes() {
UNCOV
702
        return removeFormAttributes;
×
703
    }
704

705
    /**
706
     * Sets the removes the form attributes.
707
     *
708
     * @param removeFormAttributes
709
     *            the new removes the form attributes
710
     */
711
    public void setRemoveFormAttributes(Boolean removeFormAttributes) {
712
        this.removeFormAttributes = removeFormAttributes;
×
UNCOV
713
    }
×
714

715
    /**
716
     * Gets the removes the input attributes.
717
     *
718
     * @return the removes the input attributes
719
     */
720
    public Boolean getRemoveInputAttributes() {
UNCOV
721
        return removeInputAttributes;
×
722
    }
723

724
    /**
725
     * Sets the removes the input attributes.
726
     *
727
     * @param removeInputAttributes
728
     *            the new removes the input attributes
729
     */
730
    public void setRemoveInputAttributes(Boolean removeInputAttributes) {
731
        this.removeInputAttributes = removeInputAttributes;
×
UNCOV
732
    }
×
733

734
    /**
735
     * Gets the simple boolean attributes.
736
     *
737
     * @return the simple boolean attributes
738
     */
739
    public Boolean getSimpleBooleanAttributes() {
UNCOV
740
        return simpleBooleanAttributes;
×
741
    }
742

743
    /**
744
     * Sets the simple boolean attributes.
745
     *
746
     * @param simpleBooleanAttributes
747
     *            the new simple boolean attributes
748
     */
749
    public void setSimpleBooleanAttributes(Boolean simpleBooleanAttributes) {
750
        this.simpleBooleanAttributes = simpleBooleanAttributes;
×
UNCOV
751
    }
×
752

753
    /**
754
     * Gets the removes the java script protocol.
755
     *
756
     * @return the removes the java script protocol
757
     */
758
    public Boolean getRemoveJavaScriptProtocol() {
UNCOV
759
        return removeJavaScriptProtocol;
×
760
    }
761

762
    /**
763
     * Sets the removes the java script protocol.
764
     *
765
     * @param removeJavaScriptProtocol
766
     *            the new removes the java script protocol
767
     */
768
    public void setRemoveJavaScriptProtocol(Boolean removeJavaScriptProtocol) {
769
        this.removeJavaScriptProtocol = removeJavaScriptProtocol;
×
UNCOV
770
    }
×
771

772
    /**
773
     * Gets the removes the http protocol.
774
     *
775
     * @return the removes the http protocol
776
     */
777
    public Boolean getRemoveHttpProtocol() {
UNCOV
778
        return removeHttpProtocol;
×
779
    }
780

781
    /**
782
     * Sets the removes the http protocol.
783
     *
784
     * @param removeHttpProtocol
785
     *            the new removes the http protocol
786
     */
787
    public void setRemoveHttpProtocol(Boolean removeHttpProtocol) {
788
        this.removeHttpProtocol = removeHttpProtocol;
×
UNCOV
789
    }
×
790

791
    /**
792
     * Gets the removes the https protocol.
793
     *
794
     * @return the removes the https protocol
795
     */
796
    public Boolean getRemoveHttpsProtocol() {
UNCOV
797
        return removeHttpsProtocol;
×
798
    }
799

800
    /**
801
     * Sets the removes the https protocol.
802
     *
803
     * @param removeHttpsProtocol
804
     *            the new removes the https protocol
805
     */
806
    public void setRemoveHttpsProtocol(Boolean removeHttpsProtocol) {
807
        this.removeHttpsProtocol = removeHttpsProtocol;
×
UNCOV
808
    }
×
809

810
    /**
811
     * Gets the compress css.
812
     *
813
     * @return the compress css
814
     */
815
    public Boolean getCompressCss() {
UNCOV
816
        return compressCss;
×
817
    }
818

819
    /**
820
     * Sets the compress css.
821
     *
822
     * @param compressCss
823
     *            the new compress css
824
     */
825
    public void setCompressCss(Boolean compressCss) {
826
        this.compressCss = compressCss;
×
UNCOV
827
    }
×
828

829
    /**
830
     * Gets the preserve line breaks.
831
     *
832
     * @return the preserve line breaks
833
     */
834
    public Boolean getPreserveLineBreaks() {
UNCOV
835
        return preserveLineBreaks;
×
836
    }
837

838
    /**
839
     * Sets the preserve line breaks.
840
     *
841
     * @param preserveLineBreaks
842
     *            the new preserve line breaks
843
     */
844
    public void setPreserveLineBreaks(Boolean preserveLineBreaks) {
845
        this.preserveLineBreaks = preserveLineBreaks;
×
UNCOV
846
    }
×
847

848
    /**
849
     * Gets the yui css line break.
850
     *
851
     * @return the yui css line break
852
     */
853
    public Integer getYuiCssLineBreak() {
UNCOV
854
        return yuiCssLineBreak;
×
855
    }
856

857
    /**
858
     * Sets the yui css line break.
859
     *
860
     * @param yuiCssLineBreak
861
     *            the new yui css line break
862
     */
863
    public void setYuiCssLineBreak(Integer yuiCssLineBreak) {
864
        this.yuiCssLineBreak = yuiCssLineBreak;
×
UNCOV
865
    }
×
866

867
    /**
868
     * Gets the compress java script.
869
     *
870
     * @return the compress java script
871
     */
872
    public Boolean getCompressJavaScript() {
UNCOV
873
        return compressJavaScript;
×
874
    }
875

876
    /**
877
     * Sets the compress java script.
878
     *
879
     * @param compressJavaScript
880
     *            the new compress java script
881
     */
882
    public void setCompressJavaScript(Boolean compressJavaScript) {
883
        this.compressJavaScript = compressJavaScript;
×
UNCOV
884
    }
×
885

886
    /**
887
     * Gets the js compressor.
888
     *
889
     * @return the js compressor
890
     */
891
    public String getJsCompressor() {
UNCOV
892
        return jsCompressor;
×
893
    }
894

895
    /**
896
     * Sets the js compressor.
897
     *
898
     * @param jsCompressor
899
     *            the new js compressor
900
     */
901
    public void setJsCompressor(String jsCompressor) {
902
        this.jsCompressor = jsCompressor;
×
UNCOV
903
    }
×
904

905
    /**
906
     * Gets the yui js no munge.
907
     *
908
     * @return the yui js no munge
909
     */
910
    public Boolean getYuiJsNoMunge() {
UNCOV
911
        return yuiJsNoMunge;
×
912
    }
913

914
    /**
915
     * Sets the yui js no munge.
916
     *
917
     * @param yuiJsNoMunge
918
     *            the new yui js no munge
919
     */
920
    public void setYuiJsNoMunge(Boolean yuiJsNoMunge) {
921
        this.yuiJsNoMunge = yuiJsNoMunge;
×
UNCOV
922
    }
×
923

924
    /**
925
     * Gets the yui js preserve all semi colons.
926
     *
927
     * @return the yui js preserve all semi colons
928
     */
929
    public Boolean getYuiJsPreserveAllSemiColons() {
UNCOV
930
        return yuiJsPreserveAllSemiColons;
×
931
    }
932

933
    /**
934
     * Sets the yui js preserve all semi colons.
935
     *
936
     * @param yuiJsPreserveAllSemiColons
937
     *            the new yui js preserve all semi colons
938
     */
939
    public void setYuiJsPreserveAllSemiColons(Boolean yuiJsPreserveAllSemiColons) {
940
        this.yuiJsPreserveAllSemiColons = yuiJsPreserveAllSemiColons;
×
UNCOV
941
    }
×
942

943
    /**
944
     * Gets the yui js line break.
945
     *
946
     * @return the yui js line break
947
     */
948
    public Integer getYuiJsLineBreak() {
UNCOV
949
        return yuiJsLineBreak;
×
950
    }
951

952
    /**
953
     * Sets the yui js line break.
954
     *
955
     * @param yuiJsLineBreak
956
     *            the new yui js line break
957
     */
958
    public void setYuiJsLineBreak(Integer yuiJsLineBreak) {
959
        this.yuiJsLineBreak = yuiJsLineBreak;
×
UNCOV
960
    }
×
961

962
    /**
963
     * Gets the closure opt level.
964
     *
965
     * @return the closure opt level
966
     */
967
    public String getClosureOptLevel() {
UNCOV
968
        return closureOptLevel;
×
969
    }
970

971
    /**
972
     * Sets the closure opt level.
973
     *
974
     * @param closureOptLevel
975
     *            the new closure opt level
976
     */
977
    public void setClosureOptLevel(String closureOptLevel) {
978
        this.closureOptLevel = closureOptLevel;
×
UNCOV
979
    }
×
980

981
    /**
982
     * Gets the yui js disable optimizations.
983
     *
984
     * @return the yui js disable optimizations
985
     */
986
    public Boolean getYuiJsDisableOptimizations() {
UNCOV
987
        return yuiJsDisableOptimizations;
×
988
    }
989

990
    /**
991
     * Sets the yui js disable optimizations.
992
     *
993
     * @param yuiJsDisableOptimizations
994
     *            the new yui js disable optimizations
995
     */
996
    public void setYuiJsDisableOptimizations(Boolean yuiJsDisableOptimizations) {
997
        this.yuiJsDisableOptimizations = yuiJsDisableOptimizations;
×
UNCOV
998
    }
×
999

1000
    /**
1001
     * Gets the predefined preserve patterns.
1002
     *
1003
     * @return the predefined preserve patterns
1004
     */
1005
    public String[] getPredefinedPreservePatterns() {
UNCOV
1006
        return predefinedPreservePatterns;
×
1007
    }
1008

1009
    /**
1010
     * Sets the predefined preserve patterns.
1011
     *
1012
     * @param predefinedPreservePatterns
1013
     *            the new predefined preserve patterns
1014
     */
1015
    public void setPredefinedPreservePatterns(String[] predefinedPreservePatterns) {
1016
        this.predefinedPreservePatterns = predefinedPreservePatterns;
×
UNCOV
1017
    }
×
1018

1019
    /**
1020
     * Gets the preserve patterns.
1021
     *
1022
     * @return the preserve patterns
1023
     */
1024
    public String[] getPreservePatterns() {
UNCOV
1025
        return preservePatterns;
×
1026
    }
1027

1028
    /**
1029
     * Sets the preserve patterns.
1030
     *
1031
     * @param preservePatterns
1032
     *            the new preserve patterns
1033
     */
1034
    public void setPreservePatterns(String[] preservePatterns) {
1035
        this.preservePatterns = preservePatterns;
×
UNCOV
1036
    }
×
1037

1038
    /**
1039
     * Gets the preserve pattern files.
1040
     *
1041
     * @return the preserve pattern files
1042
     */
1043
    public File[] getPreservePatternFiles() {
UNCOV
1044
        return preservePatternFiles;
×
1045
    }
1046

1047
    /**
1048
     * Sets the preserve pattern files.
1049
     *
1050
     * @param preservePatternFiles
1051
     *            the new preserve pattern files
1052
     */
1053
    public void setPreservePatternFiles(File[] preservePatternFiles) {
1054
        this.preservePatternFiles = preservePatternFiles;
×
UNCOV
1055
    }
×
1056

1057
    /**
1058
     * Gets the generate statistics.
1059
     *
1060
     * @return the generate statistics
1061
     */
1062
    public Boolean getGenerateStatistics() {
UNCOV
1063
        return generateStatistics;
×
1064
    }
1065

1066
    /**
1067
     * Sets the generate statistics.
1068
     *
1069
     * @param generateStatistics
1070
     *            the new generate statistics
1071
     */
1072
    public void setGenerateStatistics(Boolean generateStatistics) {
1073
        this.generateStatistics = generateStatistics;
×
UNCOV
1074
    }
×
1075

1076
    /**
1077
     * Gets the src folder.
1078
     *
1079
     * @return the src folder
1080
     */
1081
    public String getSrcFolder() {
UNCOV
1082
        return srcFolder;
×
1083
    }
1084

1085
    /**
1086
     * Sets the src folder.
1087
     *
1088
     * @param srcFolder
1089
     *            the new src folder
1090
     */
1091
    public void setSrcFolder(String srcFolder) {
1092
        this.srcFolder = srcFolder;
1✔
1093
    }
1✔
1094

1095
    /**
1096
     * Gets the target folder.
1097
     *
1098
     * @return the target folder
1099
     */
1100
    public String getTargetFolder() {
UNCOV
1101
        return targetFolder;
×
1102
    }
1103

1104
    /**
1105
     * Sets the target folder.
1106
     *
1107
     * @param targetFolder
1108
     *            the new target folder
1109
     */
1110
    public void setTargetFolder(String targetFolder) {
1111
        this.targetFolder = targetFolder;
1✔
1112
    }
1✔
1113

1114
    /**
1115
     * Gets the javascript html sprite.
1116
     *
1117
     * @return the javascript html sprite
1118
     */
1119
    public Boolean getJavascriptHtmlSprite() {
UNCOV
1120
        return javascriptHtmlSprite;
×
1121
    }
1122

1123
    /**
1124
     * Sets the javascript html sprite.
1125
     *
1126
     * @param javascriptHtmlSprite
1127
     *            the new javascript html sprite
1128
     */
1129
    public void setJavascriptHtmlSprite(Boolean javascriptHtmlSprite) {
1130
        this.javascriptHtmlSprite = javascriptHtmlSprite;
×
UNCOV
1131
    }
×
1132

1133
    /**
1134
     * Gets the javascript html sprite integration file.
1135
     *
1136
     * @return the javascript html sprite integration file
1137
     */
1138
    public String getJavascriptHtmlSpriteIntegrationFile() {
UNCOV
1139
        return javascriptHtmlSpriteIntegrationFile;
×
1140
    }
1141

1142
    /**
1143
     * Sets the javascript html sprite integration file.
1144
     *
1145
     * @param javascriptHtmlSpriteIntegrationFile
1146
     *            the new javascript html sprite integration file
1147
     */
1148
    public void setJavascriptHtmlSpriteIntegrationFile(String javascriptHtmlSpriteIntegrationFile) {
1149
        this.javascriptHtmlSpriteIntegrationFile = javascriptHtmlSpriteIntegrationFile;
1✔
1150
    }
1✔
1151

1152
    /**
1153
     * Gets the javascript html sprite target file.
1154
     *
1155
     * @return the javascript html sprite target file
1156
     */
1157
    public String getJavascriptHtmlSpriteTargetFile() {
UNCOV
1158
        return javascriptHtmlSpriteTargetFile;
×
1159
    }
1160

1161
    /**
1162
     * Sets the javascript html sprite target file.
1163
     *
1164
     * @param javascriptHtmlSpriteTargetFile
1165
     *            the new javascript html sprite target file
1166
     */
1167
    public void setJavascriptHtmlSpriteTargetFile(String javascriptHtmlSpriteTargetFile) {
1168
        this.javascriptHtmlSpriteTargetFile = javascriptHtmlSpriteTargetFile;
×
UNCOV
1169
    }
×
1170

1171
    /**
1172
     * Gets the encoding.
1173
     *
1174
     * @return the encoding
1175
     */
1176
    public String getEncoding() {
UNCOV
1177
        return encoding;
×
1178
    }
1179

1180
    /**
1181
     * Sets the encoding.
1182
     *
1183
     * @param encoding
1184
     *            the new encoding
1185
     */
1186
    public void setEncoding(String encoding) {
1187
        this.encoding = encoding;
×
UNCOV
1188
    }
×
1189

1190
    /**
1191
     * Gets the closure custom externs only.
1192
     *
1193
     * @return the closure custom externs only
1194
     */
1195
    public Boolean getClosureCustomExternsOnly() {
UNCOV
1196
        return closureCustomExternsOnly;
×
1197
    }
1198

1199
    /**
1200
     * Sets the closure custom externs only.
1201
     *
1202
     * @param closureCustomExternsOnly
1203
     *            the new closure custom externs only
1204
     */
1205
    public void setClosureCustomExternsOnly(Boolean closureCustomExternsOnly) {
1206
        this.closureCustomExternsOnly = closureCustomExternsOnly;
×
UNCOV
1207
    }
×
1208

1209
    /**
1210
     * Gets the closure externs.
1211
     *
1212
     * @return the closure externs
1213
     */
1214
    public String[] getClosureExterns() {
UNCOV
1215
        return closureExterns;
×
1216
    }
1217

1218
    /**
1219
     * Sets the closure externs.
1220
     *
1221
     * @param closureExterns
1222
     *            the new closure externs
1223
     */
1224
    public void setClosureExterns(String[] closureExterns) {
1225
        this.closureExterns = closureExterns;
×
UNCOV
1226
    }
×
1227
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc