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}