/* * Copyright 2013 Julien Dramaix. * * 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.google.gwt.resources.converter; import static java.lang.String.format; import com.google.common.base.Strings; import com.google.common.css.SourceCode; import com.google.common.css.compiler.ast.GssParser; import com.google.common.css.compiler.ast.GssParserException; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.TreeLogger.Type; import com.google.gwt.dev.util.TextOutput; import com.google.gwt.resources.css.ast.Context; import com.google.gwt.resources.css.ast.CssDef; import com.google.gwt.resources.css.ast.CssEval; import com.google.gwt.resources.css.ast.CssExternalSelectors; import com.google.gwt.resources.css.ast.CssFontFace; import com.google.gwt.resources.css.ast.CssIf; import com.google.gwt.resources.css.ast.CssMediaRule; import com.google.gwt.resources.css.ast.CssNoFlip; import com.google.gwt.resources.css.ast.CssPageRule; import com.google.gwt.resources.css.ast.CssProperty; import com.google.gwt.resources.css.ast.CssProperty.DotPathValue; import com.google.gwt.resources.css.ast.CssProperty.Value; import com.google.gwt.resources.css.ast.CssRule; import com.google.gwt.resources.css.ast.CssSelector; import com.google.gwt.resources.css.ast.CssSprite; import com.google.gwt.resources.css.ast.CssUnknownAtRule; import com.google.gwt.resources.css.ast.CssUrl; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Pattern; public class GssGenerationVisitor extends ExtendedCssVisitor { /* templates and tokens list */ private static final String NO_FLIP = "/* @noflip */"; private static final String GWT_SPRITE = "gwt-sprite: \"%s\""; private static final String OR = " || "; private static final String NOT = "!"; private static final String IF = "@if (%s)"; private static final String ELSE_IF = "@elseif (%s)"; private static final String ELSE = "@else "; private static final String IS = "is(\"%s\", \"%s\")"; private static final String EVAL = "eval('%s')"; private static final String VALUE = "value('%s')"; private static final String VALUE_WITH_SUFFIX = "value('%s', '%s')"; private static final String URL = "resourceUrl(\"%s\")"; private static final String DEF = "@def "; private static final String EXTERNAL = "@external"; private static final String IMPORTANT = " !important"; private static final Pattern UNESCAPE = Pattern.compile("\\\\"); private static final Pattern UNESCAPE_EXTERNAL = Pattern.compile("\\\\|@external|,|\\n|\\r"); private final Map<String, String> defKeyMapping; private final TextOutput out; private final boolean lenient; private final TreeLogger treeLogger; private final List<CssExternalSelectors> wrongExternalNodes; private final List<CssDef> wrongDefNodes; private boolean noFlip; private boolean newLine; private boolean needsOpenBrace; private boolean needsComma; private boolean inUrl; private boolean inMedia; public GssGenerationVisitor(TextOutput out, Map<String, String> defKeyMapping, boolean lenient, TreeLogger treeLogger) { this.defKeyMapping = defKeyMapping; this.out = out; this.lenient = lenient; this.treeLogger = treeLogger; newLine = true; wrongExternalNodes = new ArrayList<CssExternalSelectors>(); wrongDefNodes = new ArrayList<CssDef>(); } public String getContent() { return out.toString(); } @Override public void endVisit(CssFontFace x, Context ctx) { closeBrace(); } @Override public void endVisit(CssMediaRule x, Context ctx) { out.indentOut(); out.print("}"); out.newlineOpt(); inMedia = false; maybePrintWrongExternalNodes(); maybePrintWrongDefNodes(ctx); } @Override public void endVisit(CssPageRule x, Context ctx) { out.indentOut(); out.print("}"); out.newlineOpt(); } @Override public void endVisit(CssUnknownAtRule x, Context ctx) { out.print(x.getRule()); } @Override public boolean visit(CssSprite x, Context ctx) { return false; } @Override public void endVisit(CssSprite x, Context ctx) { needsComma = false; accept(x.getSelectors()); openBrace(); out.print(format(GWT_SPRITE, x.getResourceFunction().getPath())); semiColon(); accept(x.getProperties()); closeBrace(); } @Override public boolean visit(CssDef x, Context ctx) { printDef(x, null, "def"); return false; } @Override public boolean visit(CssEval x, Context ctx) { printDef(x, EVAL, "eval"); return false; } @Override public boolean visit(CssUrl x, Context ctx) { inUrl = true; printDef(x, URL, "url"); inUrl = false; return false; } @Override public boolean visit(CssRule x, Context ctx) { if (newLine) { out.newlineOpt(); } needsOpenBrace = true; needsComma = false; newLine = false; return true; } @Override public void endVisit(CssRule x, Context ctx) { // empty rule block case. maybePrintOpenBrace(); closeBrace(); newLine = true; } @Override public boolean visit(CssNoFlip x, Context ctx) { noFlip = true; return true; } @Override public boolean visit(CssExternalSelectors x, Context ctx) { if (inMedia) { if (lenient) { treeLogger.log(Type.WARN, "An external at-rule is not allowed inside a @media at-rule. " + "The following external at-rule [" + x + "] will be moved in the upper scope"); wrongExternalNodes.add(x); } else { treeLogger.log(Type.ERROR, "An external at-rule is not allowed inside a @media at-rule. "); } } else { printExternal(x); } return false; } private void maybePrintWrongExternalNodes() { if (!lenient) { return; } for (CssExternalSelectors external : wrongExternalNodes) { printExternal(external); } wrongExternalNodes.clear(); } private void maybePrintWrongDefNodes(Context ctx) { if (!lenient) { return; } for (CssDef def : wrongDefNodes) { if (def instanceof CssUrl) { visit((CssUrl) def, ctx); } else if (def instanceof CssEval) { visit((CssEval) def, ctx); } else { visit(def, ctx); } } wrongDefNodes.clear(); } private void printExternal(CssExternalSelectors x) { boolean first = true; for (String selector : x.getClasses()) { String unescaped = unescapeExternalClass(selector); if (validateExternalClass(selector) && !Strings.isNullOrEmpty(unescaped)) { if (first) { out.print(EXTERNAL); first = false; } out.print(" "); boolean needQuote = selector.endsWith("*"); if (needQuote) { out.print("'"); } out.printOpt(unescaped); if (needQuote) { out.print("'"); } } } if (!first) { semiColon(); } } private boolean validateExternalClass(String selector) { if (selector.contains(":")) { if (lenient) { treeLogger.log(Type.WARN, "This invalid external selector will be skipped: " + selector); return false; } else { throw new Css2GssConversionException( "One of your external statements contains a pseudo class: " + selector); } } return true; } @Override public void endVisit(CssNoFlip x, Context ctx) { noFlip = false; } @Override public boolean visit(CssProperty x, Context ctx) { maybePrintOpenBrace(); StringBuilder propertyBuilder = new StringBuilder(); if (noFlip) { propertyBuilder.append(NO_FLIP); propertyBuilder.append(' '); } propertyBuilder.append(x.getName()); propertyBuilder.append(": "); propertyBuilder.append(printValuesList(x.getValues().getValues())); if (x.isImportant()) { propertyBuilder.append(IMPORTANT); } String cssProperty = propertyBuilder.toString(); if (lenient) { // lenient mode: Try to parse the css rule and if an error occurs, // print a warning message and don't print the rule. try { new GssParser(new SourceCode(null, "body{" + cssProperty + "}")).parse(); } catch (GssParserException e) { treeLogger.log(Type.WARN, "The following property is not valid and will be skipped: " + cssProperty); return false; } } out.print(cssProperty); semiColon(); return true; } @Override public boolean visit(CssElse x, Context ctx) { closeBrace(); out.print(ELSE); openBrace(); newLine = false; return true; } @Override public boolean visit(CssElIf x, Context ctx) { closeBrace(); openConditional(ELSE_IF, x); return true; } @Override public void endVisit(CssIf x, Context ctx) { closeBrace(); newLine = true; } @Override public boolean visit(CssIf x, Context ctx) { out.newline(); openConditional(IF, x); return true; } private void openConditional(String template, CssIf ifOrElif) { String condition; String runtimeCondition = extractExpression(ifOrElif); if (runtimeCondition != null) { condition = format(EVAL, runtimeCondition); } else { condition = printConditionnalExpression(ifOrElif); } out.print(format(template, condition)); openBrace(); newLine = false; } private String extractExpression(CssIf ifOrElif) { String condition = ifOrElif.getExpression(); if (condition == null) { return null; } if (condition.trim().startsWith("(")) { condition = condition.substring(1, condition.length() - 1); } return condition; } @Override public boolean visit(CssFontFace x, Context ctx) { out.print("@font-face"); openBrace(); return true; } @Override public boolean visit(CssMediaRule x, Context ctx) { inMedia = true; out.print("@media"); boolean isFirst = true; for (String m : x.getMedias()) { if (isFirst) { out.print(" "); isFirst = false; } else { comma(); } out.print(m); } spaceOpt(); out.print("{"); out.newlineOpt(); out.indentIn(); return true; } @Override public boolean visit(CssPageRule x, Context ctx) { out.print("@page"); if (x.getPseudoPage() != null) { out.print(" :"); out.print(x.getPseudoPage()); } spaceOpt(); out.print("{"); out.newlineOpt(); out.indentIn(); return true; } @Override public boolean visit(CssSelector x, Context ctx) { if (needsComma) { comma(); } if (newLine) { out.newline(); } needsComma = true; newLine = true; out.print(unescape(x.getSelector())); return true; } private void printDef(CssDef def, String valueTemplate, String atRule) { if (validateDefNode(def, atRule)) { out.print(DEF); String name = defKeyMapping.get(def.getKey()); if (name == null) { throw new Css2GssConversionException("unknown @" + atRule + " rule [" + def.getKey() + "]"); } out.print(name); out.print(' '); String values = printValuesList(def.getValues()); if (valueTemplate != null) { out.print(format(valueTemplate, values)); } else { out.print(values); } semiColon(); } } private boolean validateDefNode(CssDef def, String atRule) { if (inMedia) { if (lenient) { treeLogger.log(Type.WARN, "A " + atRule + " is not allowed inside a @media at-rule." + "The following " + atRule + " [" + def + "] will be moved in the upper scope"); wrongDefNodes.add(def); return false; } else { treeLogger.log(Type.ERROR, "A " + atRule + " is not allowed inside a @media at-rule."); } } return true; } private void closeBrace() { out.indentOut(); out.print('}'); out.newlineOpt(); } private void comma() { out.print(','); spaceOpt(); } private void openBrace() { spaceOpt(); out.print('{'); out.newlineOpt(); out.indentIn(); } private void semiColon() { out.print(';'); out.newlineOpt(); } private void spaceOpt() { out.printOpt(' '); } private void maybePrintOpenBrace() { if (needsOpenBrace) { openBrace(); needsOpenBrace = false; } } private String printConditionnalExpression(CssIf x) { if (x == null || x.getExpression() != null) { throw new IllegalStateException(); } StringBuilder builder = new StringBuilder(); String propertyName = x.getPropertyName(); for (String propertyValue : x.getPropertyValues()) { if (builder.length() != 0) { builder.append(OR); } if (x.isNegated()) { builder.append(NOT); } builder.append(format(IS, propertyName, propertyValue)); } return builder.toString(); } private String printValuesList(List<Value> values) { StringBuilder builder = new StringBuilder(); for (Value value : values) { if (value.isSpaceRequired() && builder.length() != 0) { builder.append(' '); } String expression = value.toCss(); if (value.isIdentValue() != null && defKeyMapping.containsKey(expression)) { expression = defKeyMapping.get(expression); } else if (value.isExpressionValue() != null) { expression = value.getExpression(); } else if (value.isDotPathValue() != null) { DotPathValue dotPathValue = value.isDotPathValue(); if (inUrl) { expression = dotPathValue.getPath(); } else { if (Strings.isNullOrEmpty(dotPathValue.getSuffix())) { expression = format(VALUE, dotPathValue.getPath()); } else { expression = format(VALUE_WITH_SUFFIX, dotPathValue.getPath(), dotPathValue.getSuffix()); } } } builder.append(unescape(expression)); } return builder.toString(); } private String unescape(String toEscape) { return UNESCAPE.matcher(toEscape).replaceAll(""); } private String unescapeExternalClass(String external) { return UNESCAPE_EXTERNAL.matcher(external).replaceAll(""); } }