All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gwt.resources.converter.GssGenerationVisitor Maven / Gradle / Ivy

There is a newer version: 2.10.0
Show newest version
/*
 * Copyright 2014 Google Inc.
 *
 * 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 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.CssCharset;
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.FunctionValue;
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 com.google.gwt.thirdparty.common.css.SourceCode;
import com.google.gwt.thirdparty.common.css.compiler.ast.GssParser;
import com.google.gwt.thirdparty.common.css.compiler.ast.GssParserException;
import com.google.gwt.thirdparty.guava.common.base.Predicate;
import com.google.gwt.thirdparty.guava.common.base.Splitter;
import com.google.gwt.thirdparty.guava.common.base.Strings;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * The GssGenerationVisitor turns a css tree into a gss string.
 */
public class GssGenerationVisitor extends ExtendedCssVisitor {
  /* templates and tokens list */
  private static final String AND = " && ";
  private static final String CHARSET = "@charset \"%s\";";
  private static final String DEF = "@def ";
  private static final String ELSE = "@else ";
  private static final String ELSE_IF = "@elseif (%s)";
  private static final String EVAL = "eval('%s')";
  private static final String EXTERNAL = "@external";
  private static final String GWT_SPRITE = "gwt-sprite: \"%s\"";
  private static final String IF = "@if (%s)";
  private static final String IMPORTANT = " !important";
  private static final String IS = "is(\"%s\", \"%s\")";
  private static final String NO_FLIP = "/* @noflip */";
  private static final String NOT = "!";
  private static final String OR = " || ";
  private static final Pattern UNESCAPE = Pattern.compile("\\\\");
  private static final Pattern UNESCAPE_EXTERNAL = Pattern.compile("\\\\|@external|,|\\n|\\r");
  private static final String URL = "resourceUrl(\"%s\")";
  private static final String VALUE = "value('%s')";
  private static final String VALUE_WITH_SUFFIX = "value('%s', '%s')";

  // Used to quote font family name that contains white space(s) and aren't quoted yet.
  private static Pattern NOT_QUOTED_WITH_WITHESPACE = Pattern.compile("^[^'\"].*\\s.*[^'\"]$");

  // Used to sanitize the boolean conditions
  private static Pattern BANG_OPERATOR = Pattern.compile("^(!+)(.*)");

  // GSS impose constant names to be in uppercase. This Map will contains the mapping between
  // the name of constants defined in the CSS and the corresponding name that will be used in GSS.
  private final Map cssToGssConstantMapping;
  private final TextOutput out;
  private final boolean lenient;
  private final TreeLogger treeLogger;
  private final Predicate simpleBooleanConditionPredicate;
  // list of external at-rules defined inside a media at-rule.
  // In lenient mode, these nodes will be extracted and print outside the media at-rule.
  private final List wrongExternalNodes = new
      ArrayList();

  // list of constant definition nodes defined inside a media at-rule.
  // In lenient mode, these nodes will be extracted and print outside the media at-rule.
  private final List wrongDefNodes = new ArrayList();

  private boolean insideNoFlipNode;
  private boolean needsNewLine;
  private boolean needsOpenBrace;
  private boolean needsComma;
  private boolean insideMediaAtRule;
  // used to group a sequence of @def in one block
  private boolean previousNodeIsDef;
  // used to group asequence of @external in one block
  private boolean previousNodeIsExternal;

  public GssGenerationVisitor(TextOutput out, Map cssToGssConstantMapping,
      boolean lenient, TreeLogger treeLogger, Predicate simpleBooleanConditionPredicate) {
    this.cssToGssConstantMapping = cssToGssConstantMapping;
    this.out = out;
    this.lenient = lenient;
    this.treeLogger = treeLogger;
    this.simpleBooleanConditionPredicate = simpleBooleanConditionPredicate;
  }

  public String getContent() {
    return out.toString();
  }

  @Override
  public void endVisit(CssFontFace x, Context ctx) {
    closeBrace();
  }

  @Override
  public void endVisit(CssMediaRule x, Context ctx) {
    closeBrace();

    insideMediaAtRule = false;

    maybePrintWrongExternalNodes();
    maybePrintWrongDefNodes(ctx);
  }

  @Override
  public void endVisit(CssPageRule x, Context ctx) {
    closeBrace();
  }

  @Override
  public void endVisit(CssUnknownAtRule x, Context ctx) {
    // The old CSS resource has no support for many at-rules, like animations. There is no way
    // for us to parse them using the old CSS parser, so we will just output them as a string to
    // the GSS stylesheet and hope that the GSS parser is okay with the rule
    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(String.format(GWT_SPRITE, x.getResourceFunction().getPath()));
    semiColon();

    accept(x.getProperties());

    closeBrace();
  }

  @Override
  public boolean visit(CssDef x, Context ctx) {
    printDef(x, null, "def", false);

    previousNodeIsDef = true;
    previousNodeIsExternal = false;

    return false;
  }

  @Override
  public boolean visit(CssEval x, Context ctx) {
    printDef(x, EVAL, "eval", false);

    return false;
  }

  @Override
  public boolean visit(CssUrl x, Context ctx) {
    printDef(x, URL, "url", true);

    return false;
  }

  @Override
  public boolean visit(CssRule x, Context ctx) {
    maybePrintNewLine();

    needsOpenBrace = true;
    needsComma = false;
    needsNewLine = false;
    previousNodeIsDef = false;
    previousNodeIsExternal = false;

    return true;
  }

  @Override
  public void endVisit(CssRule x, Context ctx) {
    // empty rule block case.
    maybePrintOpenBrace();

    closeBrace();

    needsNewLine = true;
  }

  @Override
  public boolean visit(CssNoFlip x, Context ctx) {
    insideNoFlipNode = true;
    previousNodeIsDef = false;
    previousNodeIsExternal = false;
    return true;
  }

  @Override
  public boolean visit(CssExternalSelectors x, Context ctx) {
    if (insideMediaAtRule) {
      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. " +
            "[" + x + "].");
        throw new Css2GssConversionException("An external at-rule is not allowed inside a @media" +
            " at-rule.");
      }
    } else {
      printExternal(x);
    }

    return false;
  }

  @Override
  public boolean visit(CssCharset x, Context ctx) {
    out.print(String.format(CHARSET, x.getCharset()));
    out.newlineOpt();

    needsNewLine = true;
    previousNodeIsDef = false;
    previousNodeIsExternal = false;

    return true;
  }

  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) {
          if (!previousNodeIsExternal) {
            maybePrintNewLine();
          }

          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();
    }

    previousNodeIsDef = false;
    previousNodeIsExternal = true;
  }

  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) {
    insideNoFlipNode = false;
  }

  @Override
  public boolean visit(CssProperty x, Context ctx) {
    maybePrintOpenBrace();

    StringBuilder propertyBuilder = new StringBuilder();

    if (insideNoFlipNode) {
      propertyBuilder.append(NO_FLIP);
      propertyBuilder.append(' ');
    }

    propertyBuilder.append(x.getName());
    propertyBuilder.append(": ");

    String valueListCss = printValuesList(x.getValues().getValues(), false);

    if ("font-family".equals(x.getName())) {
      // Font family names containing whitespace should be quoted.
      valueListCss = quoteFontFamilyWithWhiteSpace(valueListCss);
    }

    propertyBuilder.append(valueListCss);

    if (x.isImportant()) {
      propertyBuilder.append(IMPORTANT);
    }

    String cssProperty = propertyBuilder.toString();

    // See if we can parse the rule using the GSS parser and thus verify that the
    // rule is indeed correct CSS.
    try {
      new GssParser(new SourceCode(null, "body{" + cssProperty + "}")).parse();
    } catch (GssParserException e) {
      if (lenient) {
        // print a warning message and don't print the rule.
        treeLogger.log(Type.WARN, "The following rule is not valid and will be skipped: " +
            cssProperty);
        return false;
      } else {
        treeLogger.log(Type.ERROR, "The following rule is not valid. " +
            cssProperty);
        throw new Css2GssConversionException("Invalid css rule", e);
      }
    }

    out.print(cssProperty);

    semiColon();

    return true;
  }

  /**
   * Quotes the font family names that contains white space but aren't quoted yet. thus allowing
   * usage of fonts that might be mistaken for constants. RTis is also recommended by the CSS
   * specification: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-family
   * 

It's important to notice that the converter doesn't manage the case where a constant is * used inside a font family name with whitespace. The font family name will be quoted and * won't be replaced. * {@code * @def myFontFamily Comic; * * .div { * font-family: Arial, myFontFamily sans MS; * } * } * * will be converted to: * * {@code * @def MY_FONT_FAMILY Comic; * * .div { * font-family: Arial, "MY_FONT_FAMILY sans MS"; * } * } * @param cssProperty */ private String quoteFontFamilyWithWhiteSpace(String cssProperty) { StringBuilder valueBuilder = new StringBuilder(); boolean first = true; for (String subProperty : Splitter.on(",").trimResults().omitEmptyStrings().split(cssProperty)) { if (first) { first = false; } else { valueBuilder.append(","); } if (NOT_QUOTED_WITH_WITHESPACE.matcher(subProperty).matches()) { valueBuilder.append("'" + subProperty + "'"); } else { valueBuilder.append(subProperty); } } return valueBuilder.toString(); } @Override public boolean visit(CssElse x, Context ctx) { closeBrace(); out.print(ELSE); openBrace(); needsNewLine = false; previousNodeIsDef = false; previousNodeIsExternal = 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(); needsNewLine = true; } @Override public boolean visit(CssIf x, Context ctx) { maybePrintNewLine(); openConditional(IF, x); return true; } private void openConditional(String template, CssIf ifOrElif) { String condition; String runtimeCondition = extractExpression(ifOrElif); if (runtimeCondition != null) { if (simpleBooleanConditionPredicate.apply(runtimeCondition)) { condition = runtimeCondition; } else { condition = String.format(EVAL, runtimeCondition); } } else { condition = printConditionnalExpression(ifOrElif); } out.print(String.format(template, condition)); openBrace(); needsNewLine = false; previousNodeIsDef = false; previousNodeIsExternal = 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); } // sanitize the expression. GSS doesn't accept more than one ! operator Matcher m = BANG_OPERATOR.matcher(condition); if (m.matches()) { String bangs = m.group(1); String replacement; if (bangs.length() % 2 == 0) { replacement = ""; } else { replacement = "!"; } condition = m.replaceFirst(replacement + "$2"); } return condition; } @Override public boolean visit(CssFontFace x, Context ctx) { out.print("@font-face"); openBrace(); previousNodeIsDef = false; previousNodeIsExternal = false; return true; } @Override public boolean visit(CssMediaRule x, Context ctx) { maybePrintNewLine(); insideMediaAtRule = 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(); needsNewLine = false; previousNodeIsDef = false; previousNodeIsExternal = false; 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(); previousNodeIsDef = false; previousNodeIsExternal = false; return true; } @Override public boolean visit(CssSelector x, Context ctx) { if (needsComma) { comma(false); } maybePrintNewLine(); needsComma = true; needsNewLine = true; out.print(unescape(x.getSelector())); return true; } private void printDef(CssDef def, String valueTemplate, String atRule, boolean insideUrlNode) { if (validateDefNode(def, atRule)) { if (!previousNodeIsDef) { maybePrintNewLine(); } out.print(DEF); String name = cssToGssConstantMapping.get(def.getKey()); if (name == null) { throw new Css2GssConversionException("unknown @" + atRule + " rule [" + def.getKey() + "]"); } out.print(name); out.print(' '); String values = printValuesList(def.getValues(), insideUrlNode); if (valueTemplate != null) { out.print(String.format(valueTemplate, values)); } else { out.print(values); } semiColon(); previousNodeIsDef = true; needsNewLine = true; } } private boolean validateDefNode(CssDef def, String atRule) { if (insideMediaAtRule) { 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. [" + def + "]"); throw new Css2GssConversionException("A " + atRule + " is not allowed inside a @media " + "at-rule."); } } return true; } private void closeBrace() { out.indentOut(); out.print('}'); out.newlineOpt(); } private void comma() { comma(true); } private void comma(boolean addSpace) { out.print(','); if (addSpace) { 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 void maybePrintNewLine() { if (needsNewLine) { out.newlineOpt(); } } 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) { if (x.isNegated()) { builder.append(AND); } else { builder.append(OR); } } if (x.isNegated()) { builder.append(NOT); } builder.append(String.format(IS, propertyName, propertyValue)); } return builder.toString(); } private String printValuesList(List values, boolean insideUrlNode) { StringBuilder builder = new StringBuilder(); for (Value value : values) { if (value.isSpaceRequired() && builder.length() != 0) { builder.append(' '); } String expression = value.toCss(); if (value.isIdentValue() != null && cssToGssConstantMapping.containsKey(expression)) { expression = cssToGssConstantMapping.get(expression); } else if (value.isExpressionValue() != null) { expression = value.getExpression(); } else if (value.isDotPathValue() != null) { DotPathValue dotPathValue = value.isDotPathValue(); if (insideUrlNode) { expression = dotPathValue.getPath(); } else { if (Strings.isNullOrEmpty(dotPathValue.getSuffix())) { expression = String.format(VALUE, dotPathValue.getPath()); } else { expression = String.format(VALUE_WITH_SUFFIX, dotPathValue.getPath(), dotPathValue.getSuffix()); } } } else if (value.isFunctionValue() != null) { FunctionValue functionValue = value.isFunctionValue(); // process the argument list values String arguments = printValuesList(functionValue.getValues().getValues(), insideUrlNode); expression = unescape(functionValue.getName()) + "(" + arguments + ")"; } // don't escape content of quoted string and don't escape isFunctionValue because the // arguments and the name of the functions are already unescaped if needed. if (value.isStringValue() != null || value.isFunctionValue() != null) { builder.append(expression); } else { 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(""); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy