001/* 002 * (C) Copyright 2006-2007 Nuxeo SAS <http://nuxeo.com> and others 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the Eclipse Public License v1.0 006 * which accompanies this distribution, and is available at 007 * http://www.eclipse.org/legal/epl-v10.html 008 * 009 * Contributors: 010 * Jean-Marc Orliaguet, Chalmers 011 * 012 * $Id$ 013 */ 014 015package org.nuxeo.theme.html; 016 017import java.io.IOException; 018import java.io.Reader; 019import java.io.StringReader; 020import java.io.StringWriter; 021import java.io.Writer; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.List; 025import java.util.Locale; 026import java.util.Properties; 027import java.util.regex.Matcher; 028import java.util.regex.Pattern; 029 030import org.apache.commons.logging.Log; 031import org.apache.commons.logging.LogFactory; 032import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper; 033import org.nuxeo.theme.formats.styles.Style; 034import org.nuxeo.theme.presets.PresetManager; 035import org.nuxeo.theme.presets.PresetType; 036import org.nuxeo.theme.properties.OrderedProperties; 037import org.nuxeo.theme.resources.ImageInfo; 038import org.nuxeo.theme.resources.PresetInfo; 039import org.nuxeo.theme.resources.ResourceBank; 040import org.nuxeo.theme.themes.ThemeDescriptor; 041import org.nuxeo.theme.themes.ThemeException; 042import org.nuxeo.theme.themes.ThemeManager; 043 044public final class CSSUtils { 045 046 static final Log log = LogFactory.getLog(CSSUtils.class); 047 048 private static final String EMPTY_CSS_SELECTOR = "EMPTY"; 049 050 private static final String CLASS_ATTR_PREFIX = "nxStyle"; 051 052 private static final String CSS_PROPERTIES_RESOURCE = "/nxthemes/html/styles/css.properties"; 053 054 private static final Pattern firstTagPattern = Pattern.compile("<(.*?)>", Pattern.DOTALL); 055 056 private static final Pattern otherTagsPattern = Pattern.compile("<.*?>(.*)", Pattern.DOTALL); 057 058 private static final Pattern classAttrPattern = Pattern.compile(" class=\"(.*?)\"", Pattern.DOTALL); 059 060 private static final Pattern emptyCssSelectorPattern = Pattern.compile("(.*?)\\{(.*?)\\}", Pattern.DOTALL); 061 062 private static final Pattern hexColorPattern = Pattern.compile(".*?#(\\p{XDigit}{3,6}).*?", Pattern.DOTALL); 063 064 private static final Pattern rgbColorPattern = Pattern.compile(".*?rgb\\s*\\(\\s*([0-9,\\s]+)\\s*\\).*?", 065 Pattern.DOTALL); 066 067 private static final Pattern urlPattern = Pattern.compile("^url\\s*\\([\\s,\",\']*(.*?)[\\s,\",\']*\\)$", 068 Pattern.DOTALL); 069 070 private static final Pattern partialUrlPattern = Pattern.compile("url\\s*\\([\\s,\",\']*([^/].*?)[\\s,\",\']*\\)", 071 Pattern.DOTALL); 072 073 private static final Pattern rgbDigitPattern = Pattern.compile("([0-9]{1,3},[0-9]{1,3},[0-9]{1,3})"); 074 075 private static final Properties cssProperties = new OrderedProperties(); 076 077 static { 078 org.nuxeo.theme.Utils.loadProperties(cssProperties, CSS_PROPERTIES_RESOURCE); 079 } 080 081 private CSSUtils() { 082 // This class is not supposed to be instantiated. 083 } 084 085 public static Properties getCssProperties() { 086 return cssProperties; 087 } 088 089 public static String styleToCss(final Style style, final Collection<String> viewNames, 090 final boolean ignoreViewName, final boolean ignoreClassName, final boolean indent) { 091 092 final StringBuilder sb = new StringBuilder(); 093 final StringBuilder pSb = new StringBuilder(); 094 for (String viewName : viewNames) { 095 final String className = computeCssClassName(style); 096 pSb.setLength(0); 097 boolean addSpace = false; 098 if (!ignoreClassName) { 099 pSb.append('.').append(className); 100 addSpace = true; 101 } 102 if (!ignoreViewName && !"*".equals(viewName)) { 103 pSb.append(toUpperCamelCase(viewName)); 104 addSpace = true; 105 } 106 107 for (String path : style.getPathsForView(viewName)) { 108 final Properties styleProperties = style.getPropertiesFor(viewName, path); 109 // if (styleProperties.isEmpty()) { 110 // continue; 111 // } 112 113 final String[] splitPaths = path.split(","); 114 final int len = splitPaths.length; 115 for (int i = 0; i < len; i++) { 116 sb.append(pSb); 117 if (addSpace && !"".equals(path)) { 118 sb.append(' '); 119 } 120 sb.append(splitPaths[i].trim()); 121 if (i < len - 1) { 122 sb.append(", "); 123 } 124 } 125 sb.append(" {"); 126 if (indent) { 127 sb.append('\n'); 128 } 129 130 for (String propertyName : styleProperties.stringPropertyNames()) { 131 String value = styleProperties.getProperty(propertyName); 132 if (value == null) { 133 continue; 134 } 135 if (indent) { 136 sb.append(" "); 137 } 138 sb.append(propertyName); 139 sb.append(':'); 140 if (indent) { 141 sb.append(' '); 142 } 143 sb.append(value).append(';'); 144 if (indent) { 145 sb.append('\n'); 146 } 147 } 148 sb.append("}\n"); 149 if (indent) { 150 sb.append('\n'); 151 } 152 153 } 154 } 155 return sb.toString(); 156 } 157 158 public static String insertCssClass(final String markup, final String className) { 159 final Matcher firstMatcher = firstTagPattern.matcher(markup); 160 final Matcher othersMatcher = otherTagsPattern.matcher(markup); 161 162 if (!(firstMatcher.find() && othersMatcher.find())) { 163 return markup; 164 } 165 166 // find a 'class="...."' match 167 String inBrackets = firstMatcher.group(1); 168 final Matcher classAttrMatcher = classAttrPattern.matcher(inBrackets); 169 170 // build a new 'class="..."' string 171 final StringBuilder classAttributes = new StringBuilder(); 172 if (classAttrMatcher.find()) { 173 classAttributes.append(classAttrMatcher.group(1)); 174 if (!classAttributes.toString().endsWith(" ")) { 175 classAttributes.append(' '); 176 } 177 } 178 179 // add new attributes 180 classAttributes.append(className); 181 182 if (classAttributes.length() == 0) { 183 return markup; 184 185 } 186 // remove the old 'class="..."' attributes, if there were some 187 inBrackets = inBrackets.replaceAll(classAttrPattern.toString(), ""); 188 189 // write the final markup 190 if (inBrackets.endsWith("/")) { 191 return String.format("<%s class=\"%s\" />%s", inBrackets.replaceAll("/$", "").trim(), 192 classAttributes.toString(), othersMatcher.group(1)); 193 } 194 return String.format("<%s class=\"%s\">%s", inBrackets, classAttributes.toString(), othersMatcher.group(1)); 195 196 } 197 198 public static String computeCssClassName(final Style style) { 199 String collectionName = style.getCollection(); 200 String prefix = CLASS_ATTR_PREFIX; 201 if (collectionName != null) { 202 prefix = toCamelCase(collectionName); 203 } 204 return String.format("%s%s", prefix, style.getUid()); 205 } 206 207 public static String replaceColor(String text, String before, String after) { 208 Matcher m = hexColorPattern.matcher(text); 209 text = text.trim(); 210 while (m.find()) { 211 String found = "#" + optimizeHexColor(m.group(1)); 212 if (found.equals(before)) { 213 text = text.replace(String.format("#%s", m.group(1)), after); 214 } 215 } 216 m = rgbColorPattern.matcher(text); 217 while (m.find()) { 218 String found = "#" + optimizeHexColor(rgbToHex(m.group(1))); 219 if (found.equals(before)) { 220 text = text.replace(String.format("rgb(%s)", m.group(1)), after); 221 } 222 } 223 return text; 224 } 225 226 public static String replaceImage(String text, String before, String after) { 227 text = text.trim(); 228 Matcher m = urlPattern.matcher(text); 229 if (m.matches()) { 230 String found = String.format("url(%s)", m.group(1)); 231 if (found.equals(before)) { 232 text = text.replace(String.format("url(%s)", m.group(1)), after); 233 } 234 } 235 return text; 236 } 237 238 public static String optimizeHexColor(String value) { 239 value = value.toLowerCase(); 240 if (value.length() != 6) { 241 return value; 242 } 243 if ((value.charAt(0) == value.charAt(1)) && (value.charAt(2) == value.charAt(3)) 244 && (value.charAt(4) == value.charAt(5))) { 245 return String.format("%s%s%s", value.charAt(0), value.charAt(2), value.charAt(4)); 246 } 247 return value; 248 } 249 250 public static String rgbToHex(String value) { 251 value = value.replaceAll("\\s", ""); 252 final Matcher m = rgbDigitPattern.matcher(value); 253 final StringBuffer sb = new StringBuffer(); 254 while (m.find()) { 255 final String[] rgb = m.group(1).split(","); 256 final StringBuffer hexcolor = new StringBuffer(); 257 for (String element : rgb) { 258 final int val = Integer.parseInt(element); 259 if (val < 16) { 260 hexcolor.append("0"); 261 } 262 hexcolor.append(Integer.toHexString(val)); 263 } 264 m.appendReplacement(sb, hexcolor.toString()); 265 } 266 m.appendTail(sb); 267 return sb.toString(); 268 } 269 270 public static List<String> extractCssColors(String value) { 271 final List<String> colors = new ArrayList<String>(); 272 value = value.trim(); 273 Matcher m = hexColorPattern.matcher(value); 274 while (m.find()) { 275 colors.add("#" + optimizeHexColor(m.group(1))); 276 } 277 m = rgbColorPattern.matcher(value); 278 while (m.find()) { 279 colors.add("#" + optimizeHexColor(rgbToHex(m.group(1)))); 280 } 281 return colors; 282 } 283 284 public static List<String> extractCssImages(String value) { 285 final List<String> images = new ArrayList<String>(); 286 value = value.trim(); 287 Matcher m = urlPattern.matcher(value); 288 if (m.matches()) { 289 images.add(String.format("url(%s)", m.group(1))); 290 } 291 return images; 292 } 293 294 public static String toCamelCase(final String value) { 295 if (value == null || value.trim().equals("")) { 296 return value; 297 } 298 final String newValue = value.replaceAll("[^\\p{Alnum}]+", " "); 299 final StringBuilder sb = new StringBuilder(); 300 final String[] parts = newValue.trim().split("\\s+"); 301 sb.append(parts[0].toLowerCase(Locale.ENGLISH)); 302 for (int i = 1; i < parts.length; ++i) { 303 sb.append(parts[i].substring(0, 1).toUpperCase()); 304 sb.append(parts[i].substring(1).toLowerCase(Locale.ENGLISH)); 305 } 306 return sb.toString(); 307 } 308 309 public static String toUpperCamelCase(final String value) { 310 if ("".equals(value)) { 311 return ""; 312 } 313 final String newValue = toCamelCase(value); 314 final StringBuilder sb = new StringBuilder(); 315 sb.append(newValue.substring(0, 1).toUpperCase()); 316 sb.append(newValue.substring(1)); 317 return sb.toString(); 318 } 319 320 public static String compressSource(final String source) throws ThemeException { 321 String compressedSource = source; 322 Reader in = null; 323 Writer out = null; 324 final CssCompressor compressor; 325 final int linebreakpos = -1; 326 try { 327 in = new StringReader(source); 328 out = new StringWriter(); 329 compressor = new CssCompressor(in); 330 compressor.compress(out, linebreakpos); 331 compressedSource = out.toString(); 332 333 } catch (IOException e) { 334 throw new ThemeException("Could not compress CSS", e); 335 } finally { 336 if (out != null) { 337 try { 338 out.close(); 339 } catch (IOException e) { 340 log.error(e, e); 341 } finally { 342 out = null; 343 } 344 } 345 } 346 if (in != null) { 347 try { 348 in.close(); 349 } catch (IOException e) { 350 log.error(e, e); 351 } finally { 352 in = null; 353 } 354 } 355 return compressedSource; 356 } 357 358 public static String expandPartialUrls(String text, String cssContextPath) { 359 Matcher m = partialUrlPattern.matcher(text); 360 if (!cssContextPath.endsWith("/")) { 361 cssContextPath += "/"; 362 } 363 String replacement = String.format("url(%s$1)", Matcher.quoteReplacement(cssContextPath)); 364 return m.replaceAll(replacement); 365 } 366 367 public static String expandVariables(String text, String basePath, String collectionName, 368 ThemeDescriptor themeDescriptor) { 369 370 String themeName = themeDescriptor.getName(); 371 372 if (basePath != null) { 373 text = text.replaceAll("\\$\\{basePath\\}", Matcher.quoteReplacement(basePath)); 374 text = text.replaceAll("\\$\\{org.nuxeo.ecm.contextPath\\}", Matcher.quoteReplacement(basePath)); 375 } 376 377 String contextPath = VirtualHostHelper.getContextPathProperty(); 378 379 // Replace global presets 380 for (PresetType preset : PresetManager.getGlobalPresets(null, null)) { 381 text = text.replaceAll(Pattern.quote(String.format("\"%s\"", preset.getTypeName())), 382 Matcher.quoteReplacement(preset.getValue())); 383 } 384 385 // Replace custom presets 386 for (PresetType preset : PresetManager.getCustomPresets(themeName)) { 387 text = text.replaceAll(Pattern.quote(String.format("\"%s\"", preset.getTypeName())), 388 Matcher.quoteReplacement(preset.getValue())); 389 } 390 391 // Replace presets from the current collection 392 if (collectionName != null) { 393 for (PresetType preset : PresetManager.getGlobalPresets(collectionName, null)) { 394 text = text.replaceAll(Pattern.quote(String.format("\"%s\"", preset.getTypeName())), 395 Matcher.quoteReplacement(PresetManager.resolvePresets(themeName, preset.getValue()))); 396 } 397 } 398 399 // Replace presets and images from resource banks 400 String resourceBankName = themeDescriptor.getResourceBankName(); 401 if (resourceBankName != null) { 402 ResourceBank resourceBank; 403 try { 404 resourceBank = ThemeManager.getResourceBank(resourceBankName); 405 406 for (PresetInfo preset : resourceBank.getPresets()) { 407 text = text.replaceAll(Pattern.quote(String.format("\"%s\"", preset.getTypeName())), 408 Matcher.quoteReplacement(PresetManager.resolvePresets(themeName, preset.getValue()))); 409 } 410 411 for (ImageInfo image : resourceBank.getImages()) { 412 String path = image.getPath(); 413 text = text.replaceAll(path, Matcher.quoteReplacement(String.format("%s/nxthemes-images/%s/%s", 414 contextPath, resourceBankName, path.replace(" ", "%20")))); 415 } 416 417 } catch (ThemeException e) { 418 log.warn("Could not get resources from theme bank: " + resourceBankName); 419 } 420 } 421 422 return text; 423 } 424}