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