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}