All Downloads are FREE. Search and download functionalities are using the official Maven repository.

biz.gabrys.maven.plugins.css.splitter.SplitMojo Maven / Gradle / Ivy

Go to download

Splits CSS stylesheets to smaller files ("parts") which contain maximum X rules.

There is a newer version: 2.0.3
Show newest version
/*
 * CSS Splitter Maven Plugin
 * http://css-splitter-maven-plugin.projects.gabrys.biz/
 *
 * Copyright (c) 2015 Adam Gabrys
 *
 * This file is licensed under the BSD 3-Clause (the "License").
 * You may not use this file except in compliance with the License.
 * You may obtain:
 * - a copy of the License at project page
 * - a template of the License at https://opensource.org/licenses/BSD-3-Clause
 */
package biz.gabrys.maven.plugins.css.splitter;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.apache.commons.io.FileUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.codehaus.plexus.util.StringUtils;

import biz.gabrys.maven.plugin.util.io.DestinationFileCreator;
import biz.gabrys.maven.plugin.util.io.FileScanner;
import biz.gabrys.maven.plugin.util.io.ScannerFactory;
import biz.gabrys.maven.plugin.util.io.ScannerPatternFormat;
import biz.gabrys.maven.plugin.util.parameter.ParametersLogBuilder;
import biz.gabrys.maven.plugin.util.parameter.sanitizer.LazySimpleSanitizer;
import biz.gabrys.maven.plugin.util.parameter.sanitizer.LazySimpleSanitizer.ValueContainer;
import biz.gabrys.maven.plugin.util.parameter.sanitizer.SimpleSanitizer;
import biz.gabrys.maven.plugin.util.parameter.sanitizer.ValueSanitizer;
import biz.gabrys.maven.plugin.util.timer.SystemTimer;
import biz.gabrys.maven.plugin.util.timer.Timer;
import biz.gabrys.maven.plugins.css.splitter.compressor.CodeCompressor;
import biz.gabrys.maven.plugins.css.splitter.css.Standard;
import biz.gabrys.maven.plugins.css.splitter.css.type.StyleSheet;
import biz.gabrys.maven.plugins.css.splitter.message.StylesheetMessagePrinter;
import biz.gabrys.maven.plugins.css.splitter.net.UrlEscaper;
import biz.gabrys.maven.plugins.css.splitter.split.StyleSheetSplliter;
import biz.gabrys.maven.plugins.css.splitter.steadystate.ParserOptions;
import biz.gabrys.maven.plugins.css.splitter.steadystate.ParserOptionsBuilder;
import biz.gabrys.maven.plugins.css.splitter.steadystate.SteadyStateParser;
import biz.gabrys.maven.plugins.css.splitter.token.TokenType;
import biz.gabrys.maven.plugins.css.splitter.tree.OrderedTree;
import biz.gabrys.maven.plugins.css.splitter.tree.OrderedTreeNode;
import biz.gabrys.maven.plugins.css.splitter.validation.RulesLimitValidator;
import biz.gabrys.maven.plugins.css.splitter.validation.StylePropertiesLimitValidator;

/**
 * 

* Splits CSS stylesheets to smaller files ("parts") which contain maximum * X rules. The plugin performs the following steps: *

*
    *
  1. reads source code
  2. *
  3. parses it using the CSS Parser (parser removes all comments)
  4. *
  5. splits parsed document to "parts"
  6. *
  7. builds imports' tree
  8. *
  9. writes to files
  10. *
*

* During split process the plugin can divide "standard style" and @media rules, which size is bigger * than 1, into smaller. *

*

* Example: *

* *
 * /* size is equal to 1, not splittable (size smaller than 2) */
 * @import 'file.css';
 *
 * /* size is equal to 2, not splittable (not "standard style" or @media rule) */
 * @font-face {
 *      font-family: FontFamilyName;
 *      src: url("font.woff2") format("woff2"), url("font.ttf") format("truetype");
 * }
 *
 * /* size is equal to 4, splittable */
 * .element {
 *      width: 100px;
 *      height: 200px;
 *      margin: 0;
 *      padding: 0;
 * }
 *
 * /* size is equal to 1, not splittable (size smaller than 2) */
 * selector1, selector2 > selector3 {
 *      width: 200px;
 * }
 *
 * /* size is equal to 1 (for safety), not splittable (size smaller than 2) */
 * .empty {
 * }
 *
 * /* size is equal to 1, not splittable (size smaller than 2) */
 * @media screen and (min-width: 480px) {
 * }
 *
 * /* size is equal to 4 (1 + 2 + 1), splittable */
 * @media screen and (min-width: 480px) {
 *
 *     /* size is equal to 1, not splittable (size smaller than 2) */
 *     rule {
 *          width: 100px;
 *     }
 *
 *     /* size is equal to 2, splittable */
 *     rule2 {
 *          width: 100px;
 *          height: 100px;
 *     }
 *
 *     /* size is equal to 1 (for safety), not splittable (size smaller than 2) */
 *     .empty {
 *     }
 * }
 * 
* * @since 1.0.0 */ @Mojo(name = "split", defaultPhase = LifecyclePhase.PROCESS_SOURCES, threadSafe = true) public class SplitMojo extends AbstractMojo { private static final String MAX_RULES_DEFAULT_VALUE = "4095"; private static final String MAX_RULES_LIMIT = "2147483647"; private static final String MAX_IMPORTS_DEFAULT_VALUE = "31"; private static final String MAX_IMPORTS_DEPTH_LIMIT = "4"; private static final String PART_INDEX_PARAMETER = "{index}"; /** * Defines whether to skip the plugin execution. * @since 1.0.0 */ @Parameter(property = "css.splitter.skip", defaultValue = "false") protected boolean skip; /** * Defines whether the plugin runs in verbose mode.
* Notice: always true in debug mode. * @since 1.0.0 */ @Parameter(property = "css.splitter.verbose", defaultValue = "false") protected boolean verbose; /** * Forces to always split the CSS stylesheets. By default sources are * only split when modified or the main destination file does not exist. * @since 1.0.0 */ @Parameter(property = "css.splitter.force", defaultValue = "false") protected boolean force; /** * The directory which contains the CSS stylesheets. * @since 1.0.0 */ @Parameter(property = "css.splitter.sourceDirectory", defaultValue = "${project.basedir}/src/main/css") protected File sourceDirectory; /** * Specifies where to place split CSS stylesheets. * @since 1.0.0 */ @Parameter(property = "css.splitter.outputDirectory", defaultValue = "${project.build.directory}") protected File outputDirectory; /** * Defines inclusion and exclusion fileset patterns format. Available options: *
    *
  • ant - Ant patterns
  • *
  • regex - regular expressions (use '/' as path separator)
  • *
* @since 1.0.0 */ @Parameter(property = "css.splitter.filesetPatternFormat", defaultValue = "ant") protected String filesetPatternFormat; /** * List of files to include. Specified as fileset patterns which are relative to the * source directory. See available fileset patterns * formats.
* Default value is: ["**/*.css"] for ant or * ["^.+\.css$"] for regex. * @since 1.0.0 */ @Parameter protected String[] includes = new String[0]; /** * List of files to exclude. Specified as fileset patterns which are relative to the * source directory. See available fileset patterns * formats.
* Default value is: []. * @since 1.0.0 */ @Parameter protected String[] excludes = new String[0]; /** * The maximum number of CSS rules in single "part".
* Notice: all values smaller than 1 are treated as 4095. * @since 1.0.0 */ @Parameter(property = "css.splitter.maxRules", defaultValue = MAX_RULES_DEFAULT_VALUE) protected int maxRules; /** * The plugin fails the build when a number of CSS rules in source file * exceeds this value.
* Notice: all values smaller than 1 are treated as 2147483647. * @since 1.0.0 */ @Parameter(property = "css.splitter.rulesLimit", defaultValue = MAX_RULES_LIMIT) protected int rulesLimit; /** * The maximum number of generated @import in a single file. The plugin ignores * @import operations that come from the source code.
* Notice: all values smaller than 2 are treated as 31. * @since 1.0.0 */ @Parameter(property = "css.splitter.maxImports", defaultValue = MAX_IMPORTS_DEFAULT_VALUE) protected int maxImports; /** * The plugin fails the build when a number of @import depth level exceed this value. The plugin * ignores @import operations that come from the source code.
* Notice: all values smaller than 1 are treated as 4. * @since 1.0.0 */ @Parameter(property = "css.splitter.importsDepthLimit", defaultValue = MAX_IMPORTS_DEPTH_LIMIT) protected int importsDepthLimit; /** * The CSS standard used to parse source code. Available values: * * @since 1.0.0 */ @Parameter(property = "css.splitter.standard", defaultValue = "3.0") protected String standard; /** * Defines whether the plugin runs in non-strict mode. In non-strict mode a * CSS parser adds support for non-standard structures (e.g. * @page rule inside @media).
* Notice: this functionality may stop working or be removed at any time. You should fix your code instead of * relying on this functionality. * @since 1.0.0 */ @Parameter(property = "css.splitter.nonstrict", defaultValue = "false") protected boolean nonstrict; /** * Defines whether the plugin allows to use "star hack" in stylesheets. "Star hack" works only in Internet Explorer * browsers - versions 7 and older. Example: * *
     * color {
     *   color: red;   /* all browsers */
     *   *color: blue; /* IE7 and below */
     * }
     * 
* * Notice: ignored when standard is equal to 1.0 or 2.0. * @since 1.2.0 */ @Parameter(property = "css.splitter.starHackAllowed", defaultValue = "false") protected boolean starHackAllowed; /** * Whether the plugin should use YUI Compressor to compress the * CSS code. * @since 1.1.0 */ @Parameter(property = "css.splitter.compress", defaultValue = "false") protected boolean compress; /** * Defines column number after which the plugin will insert a line break. From * YUI Compressor documentation:
Some source control * tools do not like files containing lines longer than, say 8000 characters. The line break option is used in that * case to split long lines after a specific column. Specify 0 to get a line break after each rule in * CSS.
Notice: all values smaller than 0 means - do not split the line after any * column. * @since 1.1.0 */ @Parameter(property = "css.splitter.compressLineBreak", defaultValue = "-1") protected int compressLineBreak; /** * Defines cache token type which will be added to @import links in destination * CSS stylesheets. Available options: *
    *
  • custom - text specified by the user
  • *
  • date - build date
  • *
  • none - token will not be added
  • *
* @since 1.0.0 */ @Parameter(property = "css.splitter.cacheTokenType", defaultValue = "none") protected String cacheTokenType; /** * Defines cache token parameter name which will be added to @import links in destination * CSS stylesheets.
* Notice: ignored when cache token type is equal to none. * @since 1.0.0 */ @Parameter(property = "css.splitter.cacheTokenParameter", defaultValue = "v") protected String cacheTokenParameter; /** * Stores different values depending on the cache token type: *
    *
  • custom - user specified value (e.g. constantText, ${variable})
  • *
  • date - pattern for {@link java.text.SimpleDateFormat} object
  • *
  • none - ignored
  • *
* Default value is: yyyyMMddHHmmss if cache token type is equal to * date.
* Required: YES if cache token type is equal to custom. * @since 1.0.0 */ @Parameter(property = "css.splitter.cacheTokenValue") protected String cacheTokenValue; /** * Sources encoding. * @since 1.0.0 */ @Parameter(property = "css.splitter.encoding", defaultValue = "${project.build.sourceEncoding}") protected String encoding; /** * Destination files naming pattern. {fileName} is equal to source file name without extension. * @since 1.0.0 */ @Parameter(property = "css.splitter.outputFileNamePattern", defaultValue = DestinationFileCreator.FILE_NAME_PARAMETER + ".css") protected String outputFileNamePattern; /** * Destination "parts" naming pattern. {fileName} is equal to source file name without extension, {index} is equal * to "part" index (first is equal to 1). "Parts" are loaded in the browsers according to indexes. For correct * listing files on all operating systems indexes can contain leading zeros. * @since 1.0.0 */ @Parameter(property = "css.splitter.outputPartNamePattern", defaultValue = DestinationFileCreator.FILE_NAME_PARAMETER + '-' + PART_INDEX_PARAMETER + ".css") protected String outputPartNamePattern; private String resolvedCacheToken = ""; private void logParameters() { if (!getLog().isDebugEnabled()) { return; } final ParametersLogBuilder logger = new ParametersLogBuilder(getLog()); logger.append("skip", skip); logger.append("verbose", verbose, new SimpleSanitizer(verbose, Boolean.TRUE)); logger.append("force", force); logger.append("sourceDirectory", sourceDirectory); logger.append("outputDirectory", outputDirectory); logger.append("filesetPatternFormat", filesetPatternFormat); logger.append("includes", includes, new LazySimpleSanitizer(includes.length != 0, new ValueContainer() { @Override public Object getValue() { return getDefaultIncludes(); } })); logger.append("excludes", excludes); logger.append("maxRules", maxRules, new SimpleSanitizer(maxRules > 0, MAX_RULES_DEFAULT_VALUE)); logger.append("rulesLimit", rulesLimit, new SimpleSanitizer(rulesLimit > 0, MAX_RULES_LIMIT)); logger.append("maxImports", maxImports, new SimpleSanitizer(maxImports >= OrderedTree.MIN_NUMBER_OF_CHILDREN, MAX_IMPORTS_DEFAULT_VALUE)); logger.append("importsDepthLimit", importsDepthLimit, new SimpleSanitizer(importsDepthLimit > 0, MAX_IMPORTS_DEPTH_LIMIT)); logger.append("standard", standard); logger.append("nonstrict", nonstrict); logger.append("starHackAllowed", starHackAllowed, new SimpleSanitizer( !starHackAllowed || !(Standard.VERSION_2_0.isSameAs(standard) || Standard.VERSION_1_0.isSameAs(standard)), Boolean.FALSE)); logger.append("compress", compress); logger.append("compressLineBreak", compressLineBreak); logger.append("cacheTokenType", cacheTokenType); logger.append("cacheTokenParameter", cacheTokenParameter); logger.append("cacheTokenValue", cacheTokenValue, new ValueSanitizer() { private Object sanitizedValue; @Override public boolean isValid(final Object value) { if (cacheTokenValue != null) { return true; } sanitizedValue = getDefaultCacheTokenValue(); return sanitizedValue == null; } @Override public Object sanitize(final Object value) { return sanitizedValue; } }); logger.append("encoding", encoding); logger.append("outputFileNamePattern", outputFileNamePattern); logger.append("outputPartNamePattern", outputPartNamePattern); logger.debug(); } private String[] getDefaultIncludes() { if (ScannerPatternFormat.ANT.name().equalsIgnoreCase(filesetPatternFormat)) { return new String[] { "**/*.css" }; } else { return new String[] { "^.+\\.css$" }; } } private String getDefaultCacheTokenValue() { if (TokenType.DATE.name().equalsIgnoreCase(cacheTokenType)) { return "yyyyMMddHHmmss"; } else { return null; } } private void logNonstricWarning() { getLog().warn("#################### NON-STRICT MODE ENABLED ####################"); getLog().warn("This functionality may stop working or be removed at any time!"); getLog().warn("You should fix your code instead of relying on this functionality."); getLog().warn("#################### NON-STRICT MODE ENABLED ####################"); } private void calculateParameters() { if (getLog().isDebugEnabled()) { verbose = true; } if (includes.length == 0) { includes = getDefaultIncludes(); } if (maxRules < 1) { maxRules = Integer.parseInt(MAX_RULES_DEFAULT_VALUE); } if (rulesLimit < 1) { rulesLimit = Integer.parseInt(MAX_RULES_LIMIT); } if (maxImports < OrderedTree.MIN_NUMBER_OF_CHILDREN) { maxImports = Integer.parseInt(MAX_IMPORTS_DEFAULT_VALUE); } if (importsDepthLimit < 1) { importsDepthLimit = Integer.parseInt(MAX_IMPORTS_DEPTH_LIMIT); } if (starHackAllowed && (Standard.VERSION_2_0.isSameAs(standard) || Standard.VERSION_1_0.isSameAs(standard))) { starHackAllowed = false; } if (cacheTokenValue == null) { cacheTokenValue = getDefaultCacheTokenValue(); } } private void validateParameters() throws MojoExecutionException { if (cacheTokenValue == null && TokenType.CUSTOM.name().equalsIgnoreCase(cacheTokenType)) { throw new MojoExecutionException("Parameter cacheTokenValue is required when cacheTokenType is equal to \"custom\"!"); } } @Override public void execute() throws MojoExecutionException, MojoFailureException { logParameters(); if (nonstrict) { logNonstricWarning(); } if (skip) { getLog().info("Skipping job execution"); return; } calculateParameters(); validateParameters(); runSplitter(); if (nonstrict) { logNonstricWarning(); } } private void runSplitter() throws MojoFailureException { if (!sourceDirectory.exists()) { getLog().warn("Source directory does not exist: " + sourceDirectory.getAbsolutePath()); return; } final Collection files = getFiles(); if (files.isEmpty()) { getLog().warn("No sources to split"); return; } resolveCacheToken(); splitFiles(files); } private Collection getFiles() { final ScannerPatternFormat patternFormat = ScannerPatternFormat.toPattern(filesetPatternFormat); final FileScanner scanner = new ScannerFactory().create(patternFormat, getLog()); if (verbose) { getLog().info("Scanning directory for sources..."); } return scanner.getFiles(sourceDirectory, includes, excludes); } private void resolveCacheToken() { if (getLog().isDebugEnabled()) { getLog().debug("Resolving cache token..."); } if (!TokenType.NONE.name().equalsIgnoreCase(cacheTokenType)) { final String value = TokenType.create(cacheTokenType).createFactory().create(cacheTokenValue); final StringBuilder cacheToken = new StringBuilder(); cacheToken.append(UrlEscaper.escape(cacheTokenParameter)); cacheToken.append('='); cacheToken.append(UrlEscaper.escape(value)); resolvedCacheToken = cacheToken.toString(); } if (verbose) { if (StringUtils.isEmpty(resolvedCacheToken)) { getLog().info("Cache token is empty"); } else { getLog().info("Cache token: " + resolvedCacheToken); } } } private void splitFiles(final Collection sources) throws MojoFailureException { final String sourceFilesText = "source file" + (sources.size() != 1 ? "s" : ""); getLog().info("Splitting " + sources.size() + ' ' + sourceFilesText + " to " + outputDirectory.getAbsolutePath()); final Timer timer = SystemTimer.getStartedTimer(); for (final File source : sources) { if (isCompilationRequired(source)) { splitFile(source); } else if (verbose) { getLog().info("Skipping stylesheet split (not modified): " + source.getAbsolutePath()); } } getLog().info("Finished " + sourceFilesText + " split in " + timer.stop()); } private boolean isCompilationRequired(final File source) { if (force) { return true; } final File destination = new DestinationFileCreator(sourceDirectory, outputDirectory, outputFileNamePattern).create(source); if (!destination.exists()) { return true; } return source.lastModified() > destination.lastModified(); } private void splitFile(final File source) throws MojoFailureException { Timer timer = null; if (verbose) { getLog().info("Splitting stylesheet: " + source.getAbsolutePath()); timer = SystemTimer.getStartedTimer(); } final String css = readCss(source); final StyleSheet stylesheet = parseStyleSheet(css); validateStyleSheet(stylesheet); final List parts = splitToParts(stylesheet); validateImportsDepth(parts); List texts = convertToText(parts); if (compress) { texts = compressStyleSheets(texts); } saveParts(source, convertToTree(texts)); if (timer != null) { getLog().info("Finished in " + timer.stop()); } } private String readCss(final File file) throws MojoFailureException { try { return FileUtils.readFileToString(file, encoding); } catch (final IOException e) { throw new MojoFailureException("Cannot read file: " + file.getAbsolutePath(), e); } } private StyleSheet parseStyleSheet(final String css) { if (getLog().isDebugEnabled()) { getLog().debug("Parsing stylesheet..."); } final ParserOptions options = new ParserOptionsBuilder().withStandard(Standard.create(standard)).withStrict(!nonstrict) .withStarHack(starHackAllowed).create(); final StyleSheet stylesheet = new SteadyStateParser(getLog()).parse(css, options); new StylesheetMessagePrinter(getLog(), !nonstrict).print(stylesheet); if (verbose) { getLog().info(String.format("Stylesheet contains %d rule%s.", stylesheet.getSize(), stylesheet.getSize() != 1 ? 's' : "")); } return stylesheet; } private List splitToParts(final StyleSheet stylesheet) { if (getLog().isDebugEnabled()) { getLog().debug("Splitting stylesheet to parts..."); } final List parts = new StyleSheetSplliter(maxRules).split(stylesheet); if (verbose) { getLog().info(String.format("Split to %d stylesheet%s.", parts.size(), parts.size() == 1 ? "" : "s")); } return parts; } private void validateImportsDepth(final List stylesheets) throws MojoFailureException { final OrderedTree tree = convertToTree(stylesheets); final int depth = tree.getDepth(); if (verbose) { getLog().info("Imports depth: " + depth); } if (getLog().isDebugEnabled()) { getLog().debug("Validating imports depth..."); } if (depth > importsDepthLimit) { throw new MojoFailureException( String.format("The number of @import depth (%d) exceeded the allowable limit (%d)!", depth, importsDepthLimit)); } } private OrderedTree convertToTree(final List objects) { return new OrderedTree(objects, maxImports); } private static List convertToText(final List stylesheets) { final List texts = new ArrayList(stylesheets.size()); for (final StyleSheet stylesheet : stylesheets) { texts.add(stylesheet.toString()); } return texts; } private List compressStyleSheets(final List stylesheets) { if (verbose) { getLog().info("Compressing CSS code..."); } final List compressed = new ArrayList(stylesheets.size()); final CodeCompressor compressor = new CodeCompressor(compressLineBreak); for (int i = 0; i < stylesheets.size(); ++i) { if (getLog().isDebugEnabled()) { getLog().debug(String.format("Compressing stylesheet no. %d...", i + 1)); } compressed.add(compressor.compress(stylesheets.get(i))); } return compressed; } private void validateStyleSheet(final StyleSheet stylesheet) { if (getLog().isDebugEnabled()) { getLog().debug("Validating stylesheet..."); } new RulesLimitValidator(rulesLimit).validate(stylesheet); new StylePropertiesLimitValidator(maxRules).validate(stylesheet); } private void saveParts(final File source, final OrderedTreeNode stylesheetsTree) throws MojoFailureException { if (getLog().isDebugEnabled()) { getLog().debug("Creating imports' tree..."); } if (verbose) { getLog().info("Saving CSS code..."); } final int numberOfDigits = String.valueOf(stylesheetsTree.size()).length(); final String indexPattern = "%0" + numberOfDigits + 'd'; saveStyleSheetsTree(source, stylesheetsTree, indexPattern); } private void saveStyleSheetsTree(final File source, final OrderedTreeNode node, final String indexPattern) throws MojoFailureException { final DestinationFileCreator fileCreator = new DestinationFileCreator(sourceDirectory, outputDirectory); if (node.getOrder() == 0) { fileCreator.setFileNamePattern(outputFileNamePattern); } else { final String index = String.format(indexPattern, node.getOrder()); fileCreator.setFileNamePattern(outputPartNamePattern.replace(PART_INDEX_PARAMETER, index)); } final File target = fileCreator.create(source); if (node.hasValue()) { saveCss(target, node.getValue()); return; } final StringBuilder imports = new StringBuilder(); for (final OrderedTreeNode child : node.getChildren()) { final String index = String.format(indexPattern, child.getOrder()); fileCreator.setFileNamePattern(outputPartNamePattern.replace(PART_INDEX_PARAMETER, index)); final File childTarget = fileCreator.create(source); saveStyleSheetsTree(source, child, indexPattern); final String parameters = StringUtils.isEmpty(resolvedCacheToken) ? "" : '?' + resolvedCacheToken; imports.append(String.format("@import \"%s%s\";%n", childTarget.getName(), parameters)); } saveCss(target, imports.toString()); } private void saveCss(final File file, final String css) throws MojoFailureException { if (getLog().isDebugEnabled()) { getLog().debug("Saving stylesheet to " + file.getAbsolutePath()); } try { FileUtils.write(file, css, encoding); } catch (final IOException e) { throw new MojoFailureException("Cannot save file: " + file.getAbsolutePath(), e); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy