/** * Copyright 2007 Jordi Hernández Sellés * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * either express or implied. See the License for the specific language governing permissions * and limitations under the License. */ package com.idega.util.resources; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.springframework.beans.factory.annotation.Autowired; import com.idega.idegaweb.include.ExternalLink; import com.idega.idegaweb.include.StyleSheetLink; import com.idega.util.CoreConstants; import com.idega.util.StringHandler; import com.idega.util.StringUtil; import com.idega.util.expression.ELUtil; /** * Minifies CSS files by removing expendable whitespace and comments. * * @author Valdas Žemaitis * */ public class CSSMinifier implements AbstractMinifier { // This regex captures comments private static final String commentRegex ="(/\\*[^*]*\\*+([^/][^*]*\\*+)*/)"; // Captures CSS strings private static final String quotedContentRegex = "('(\\\\'|[^'])*')|(\"(\\\\\"|[^\"])*\")"; // A placeholder string to replace and restore CSS strings private static final String STRING_PLACEHOLDER = "______'COMPRESSED_CSS'______"; // Captured CSS rules (requires replacing CSS strings with a placeholder, or quoted braces will fool it. private static final String rulesRegex = "([^\\{\\}]*)(\\{[^\\{\\}]*})"; // Captures newlines and tabs private static final String newlinesTabsRegex = "\\r|\\n|\\t|\\f"; // This regex captures, in order: //(\\s*\\{\\s*)|(\\s*\\}\\s*)|(\\s*\\(\\s*)|(\\s*;\\s*)|(\\s*\\)) // brackets, parentheses,colons and semicolons and any spaces around them (except spaces AFTER a parentheses closing symbol), //and ( +) occurrences of one or more spaces. private static final String spacesRegex = "(\\s*\\{\\s*)|(\\s*\\}\\s*)|(\\s*\\(\\s*)|(\\s*;\\s*)|(\\s*:\\s*)|(\\s*\\))|( +)"; private static final Pattern commentsPattern = Pattern.compile(commentRegex, Pattern.DOTALL); private static final Pattern spacesPattern = Pattern.compile(spacesRegex, Pattern.DOTALL); private static final Pattern quotedContentPattern = Pattern.compile(quotedContentRegex, Pattern.DOTALL); private static final Pattern rulesPattern = Pattern.compile(rulesRegex, Pattern.DOTALL); protected static final Pattern newlinesTabsPattern = Pattern.compile(newlinesTabsRegex, Pattern.DOTALL); private static final Pattern stringPlaceholderPattern = Pattern.compile(STRING_PLACEHOLDER, Pattern.DOTALL); private static final String SPACE = " "; private static final String BRACKET_OPEN = "{"; private static final String BRACKET_CLOSE = "}"; private static final String PAREN_OPEN = "("; private static final String PAREN_CLOSE = ")"; private static final String COLON = ":"; private static final String SEMICOLON = ";"; @Autowired private ResourceScanner resourceScanner; /** * Template class to abstract the pattern of iterating over a Matcher and performing * string replacement. */ public abstract class MatcherProcessorCallback { String processWithMatcher(Matcher matcher){ StringBuffer sb = new StringBuffer(); while(matcher.find()){ matcher.appendReplacement(sb, matchCallback(matcher)); } matcher.appendTail(sb); return sb.toString(); } abstract String matchCallback(Matcher matcher); } /** * @param data CSS to minify * @return StringBuffer Minified CSS. */ public StringBuffer minifyCSS(StringBuffer data) { // Remove comments and carriage returns String compressed = commentsPattern.matcher(data.toString()).replaceAll(""); // Temporarily replace the strings with a placeholder final List<String> strings = new ArrayList<String>(); Matcher stringMatcher = quotedContentPattern.matcher(compressed); compressed = new MatcherProcessorCallback(){ @Override String matchCallback(Matcher matcher) { String match = matcher.group(); strings.add(match); return STRING_PLACEHOLDER; }}.processWithMatcher(stringMatcher); // Grab all rules and replace whitespace in selectors Matcher rulesmatcher = rulesPattern.matcher(compressed); compressed = new MatcherProcessorCallback(){ @Override String matchCallback(Matcher matcher) { String match = matcher.group(1); String spaced = newlinesTabsPattern.matcher(match.toString()).replaceAll(SPACE).trim(); return spaced + matcher.group(2); }}.processWithMatcher(rulesmatcher); // Replace all linefeeds and tabs compressed = newlinesTabsPattern.matcher(compressed).replaceAll(""); // Match all empty space we can minify Matcher matcher = spacesPattern.matcher(compressed); compressed = new MatcherProcessorCallback(){ @Override String matchCallback(Matcher matcher) { String replacement = SPACE; String match = matcher.group(); if(match.indexOf(BRACKET_OPEN) != -1) replacement = BRACKET_OPEN; else if(match.indexOf(BRACKET_CLOSE) != -1) replacement = BRACKET_CLOSE; else if(match.indexOf(PAREN_OPEN) != -1) replacement = PAREN_OPEN; else if(match.indexOf(COLON) != -1) replacement = COLON; else if(match.indexOf(SEMICOLON) != -1) replacement = SEMICOLON; else if(match.indexOf(PAREN_CLOSE) != -1) replacement = PAREN_CLOSE; return replacement; }}.processWithMatcher(matcher); // Restore all Strings Matcher restoreMatcher = stringPlaceholderPattern.matcher(compressed); final Iterator<String> it = strings.iterator(); compressed = new MatcherProcessorCallback(){ @Override String matchCallback(Matcher matcher) { return it.next(); }}.processWithMatcher(restoreMatcher); return new StringBuffer(compressed); } private String getParsedContent(String resourceUri, String content) { if (!StringUtil.isEmpty(resourceUri)) { if (!resourceUri.startsWith(CoreConstants.SLASH)) { resourceUri = CoreConstants.SLASH.concat(resourceUri); } resourceUri = resourceUri.substring(0, resourceUri.lastIndexOf(CoreConstants.SLASH) + 1); } return getResourceScanner().getParsedContent(StringUtil.getLinesFromString(content), resourceUri); } private String getMinifiedResource(String resourceUri, String content, String media) { if (StringUtil.isEmpty(content)) { return null; } StringBuilder minified = new StringBuilder("\n/***************** STARTS: ").append(resourceUri).append(" *****************/\n"); boolean mediaIsEmpty = StringUtil.isEmpty(media); if (!mediaIsEmpty) { minified.append("@media ").append(media).append(" {\n"); } minified.append(getParsedContent(resourceUri, content)); if (!mediaIsEmpty) { minified.append("\n}"); } minified.append("\n/***************** ENDS: ").append(resourceUri).append(" *****************/\n").toString(); return minified.toString(); } public String getMinifiedResource(ExternalLink resource) { if (!(resource instanceof StyleSheetLink)) { Logger.getLogger(getClass().getName()).warning("Resource " + resource + " is not type of " + StyleSheetLink.class); return null; } StyleSheetLink cssResource = (StyleSheetLink) resource; try { return getMinifiedResource(cssResource.getUrl(), StringUtil.isEmpty(resource.getContent()) ? StringHandler.getContentFromInputStream(cssResource.getContentStream()) : resource.getContent(), cssResource.getMedia()); } catch (Exception e) { e.printStackTrace(); } return null; } public ResourceScanner getResourceScanner() { if (resourceScanner == null) { ELUtil.getInstance().autowire(this); } return resourceScanner; } public void setResourceScanner(ResourceScanner resourceScanner) { this.resourceScanner = resourceScanner; } }