/** * Copyright Alex Objelean */ package ro.isdc.wro.model.resource.processor.support; import java.io.Writer; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.regex.PatternSyntaxException; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Css minify barryvan implementation. */ public class CSSMin { private static final Logger LOG = LoggerFactory.getLogger(CSSMin.class); public void formatFile(final String input, final Writer writer) throws Exception { try { final StringBuilder sb = new StringBuilder(input.replaceAll("[\t\n\r]", "").replaceAll(" ", " ")); int k, n; // Find the start of the comment while ((n = sb.indexOf("/*")) != -1) { k = sb.indexOf("*/", n + 2); if (k == -1) { throw new Exception("Error: Unterminated comment."); } sb.delete(n, k + 2); } final List<Selector> selectors = new LinkedList<Selector>(); n = 0; while ((k = sb.indexOf("}", n)) != -1) { try { selectors.add(new Selector(sb.substring(n, k + 1))); } catch (final Exception e) { // skip } n = k + 1; } for (Selector selector : selectors) { writer.write(selector.toString()); } writer.write("\r\n"); writer.flush(); } catch (final Exception e) { LOG.error(e.getMessage(), e); } } } class Selector { private static final Logger LOG = LoggerFactory.getLogger(CSSMin.class); private final Property[] properties; private final String selector; /** * Creates a new Selector using the supplied strings. * * @param selector The selector; for example, "div { border: solid 1px red; color: blue; }" */ public Selector(final String selector) throws Exception { final String[] parts = selector.split("\\{"); // We have to escape the { with a \ for the regex, which itself // requires escaping for the string. Sigh. if (parts.length < 2) { throw new Exception("Warning: Incomplete selector: " + selector); } this.selector = parts[0].trim(); String contents = parts[1].trim(); if (contents.length() <= 1) { throw new Exception("Warning: Empty selector body: " + selector); } if (contents.charAt(contents.length() - 1) != '}') { // Ensure that we have a leading and trailing brace. throw new Exception("Warning: Unterminated selector: " + selector); } contents = StringUtils.substringBefore(contents, "}"); properties = parseProperties(contents); sortProperties(properties); } /** * Prints out this selector and its contents nicely, with the contents sorted alphabetically. */ @Override public String toString() { final StringBuilder sb = new StringBuilder(); sb.append(selector).append("{"); for (final Property property : properties) { if (property != null) { sb.append(property.toString()); } } sb.append("}"); return sb.toString(); } /** * Parses out the properties of a selector's body. * * @param contents The body; for example, "border: solid 1px red; color: blue;" */ private Property[] parseProperties(final String contents) { final String[] parts = contents.split(";"); final List<Property> resultsAsList = new ArrayList<Property>(); for (String part : parts) { try { // ignore empty parts if (!StringUtils.isEmpty(part.trim())) { resultsAsList.add(new Property(part)); } } catch (final Exception e) { LOG.warn(e.getMessage(), e); } } return resultsAsList.toArray(new Property[resultsAsList.size()]); } private void sortProperties(final Property[] propertiesToSort) { Arrays.sort(propertiesToSort); } } class Property implements Comparable { private static final Logger LOG = LoggerFactory.getLogger(CSSMin.class); protected String property; protected Value[] values; /** * Creates a new Property using the supplied strings. Parses out the values of the property selector. * * @param property The property; for example, "border: solid 1px red;" or * "-moz-box-shadow: 3px 3px 3px rgba(255, 255, 0, 0.5);". */ public Property(final String property) throws Exception { try { // Parse the property. //final String[] parts = property.split(":"); // Split "color: red" to ["color", " red"] final List<String> parts = Arrays.asList(property.split(":", 2)); // Split "color: red" to ["color", " red"] final List<String> nonEmptyParts = new ArrayList<String>(); for (String part : parts) { if (!StringUtils.isEmpty(part.trim())) { nonEmptyParts.add(part.trim()); } } if (nonEmptyParts.size() < 2) { throw new Exception("Warning: Incomplete property: " + property); } this.property = nonEmptyParts.get(0).toLowerCase(); values = parseValues(nonEmptyParts.get(1).replaceAll(", ", ",")); } catch (final PatternSyntaxException e) { // Invalid regular expression used. } } /** * Prints out this property nicely, with the contents sorted in a standardised order. */ @Override public String toString() { final StringBuilder sb = new StringBuilder(); sb.append(property).append(":"); for (final Value v : values) { sb.append(v.toString()).append(","); } sb.deleteCharAt(sb.length() - 1); // Delete the trailing comma. sb.append(";"); return sb.toString(); } public int compareTo(final Object other) { return property.compareTo(((Property)other).property); } private Value[] parseValues(final String contents) { final String[] parts = contents.split(","); final Value[] results = new Value[parts.length]; for (int i = 0; i < parts.length; i++) { try { results[i] = new Value(parts[i]); } catch (final Exception e) { LOG.error(e.getMessage(), e); results[i] = null; } } return results; } } class Value { String[] parts; public Value(final String value) throws Exception { // Parse the value. parts = value.split(" "); // Split "solid 1px red" to ["solid","1px","red"] and sort them. } @Override public String toString() { final StringBuilder sb = new StringBuilder(); for (final String part : parts) { sb.append(part).append(" "); } return sb.toString().trim(); } }