| /* |
| * 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.gss; |
| |
| import static com.google.gwt.thirdparty.common.css.compiler.passes.PassUtil.ALTERNATE; |
| |
| import com.google.gwt.core.ext.typeinfo.JClassType; |
| import com.google.gwt.core.ext.typeinfo.JMethod; |
| import com.google.gwt.core.ext.typeinfo.JType; |
| import com.google.gwt.core.ext.typeinfo.NotFoundException; |
| import com.google.gwt.resources.client.ImageResource; |
| import com.google.gwt.resources.client.ImageResource.ImageOptions; |
| import com.google.gwt.resources.client.ImageResource.RepeatStyle; |
| import com.google.gwt.resources.ext.ResourceContext; |
| import com.google.gwt.resources.ext.ResourceGeneratorUtil; |
| import com.google.gwt.resources.gss.ast.CssDotPathNode; |
| import com.google.gwt.thirdparty.common.css.SourceCodeLocation; |
| import com.google.gwt.thirdparty.common.css.compiler.ast.CssCommentNode; |
| import com.google.gwt.thirdparty.common.css.compiler.ast.CssCompilerPass; |
| import com.google.gwt.thirdparty.common.css.compiler.ast.CssDeclarationNode; |
| import com.google.gwt.thirdparty.common.css.compiler.ast.CssFunctionArgumentsNode; |
| import com.google.gwt.thirdparty.common.css.compiler.ast.CssFunctionNode; |
| import com.google.gwt.thirdparty.common.css.compiler.ast.CssFunctionNode.Function; |
| import com.google.gwt.thirdparty.common.css.compiler.ast.CssLiteralNode; |
| import com.google.gwt.thirdparty.common.css.compiler.ast.CssPropertyNode; |
| import com.google.gwt.thirdparty.common.css.compiler.ast.CssPropertyValueNode; |
| import com.google.gwt.thirdparty.common.css.compiler.ast.CssValueNode; |
| import com.google.gwt.thirdparty.common.css.compiler.ast.DefaultTreeVisitor; |
| import com.google.gwt.thirdparty.common.css.compiler.ast.ErrorManager; |
| import com.google.gwt.thirdparty.common.css.compiler.ast.GssError; |
| import com.google.gwt.thirdparty.common.css.compiler.ast.MutatingVisitController; |
| import com.google.gwt.thirdparty.guava.common.annotations.VisibleForTesting; |
| import com.google.gwt.thirdparty.guava.common.collect.ImmutableList; |
| import com.google.gwt.thirdparty.guava.common.collect.ImmutableList.Builder; |
| import com.google.gwt.thirdparty.guava.common.collect.Lists; |
| |
| import java.util.List; |
| |
| /** |
| * Visitor that detects sprite definitions and replace them by several css rules in order to create |
| * the corresponding sprited image. |
| * <p>This visitor will replace the following gss: |
| * {@code |
| * .foo { |
| * padding: 5px; |
| * gwt-sprite: imageResource; |
| * width: 150px; |
| * } |
| * } |
| * to the corresponding gss: |
| * {@code |
| * .foo { |
| * padding: 5px; |
| * /* @alternate */ width: eval("imageResource.getWidth", "px"); |
| * /* @alternate */ height: eval("imageResource.getHeight", "px"); |
| * /* @alternate */ overflow: hidden; |
| * /* @alternate */ background: resourceUrl("imageResource") eval("imageResource.getLeft", |
| * "px") eval("imageResource.getTop", "px") no-repeat; |
| * width: 150px; |
| * } |
| * } |
| * <p>This visitor will also check the presence of the {@link ImageOptions} annotation on the |
| * image resource in order to support correctly horizontal or vertical repetition. |
| */ |
| public class ImageSpriteCreator extends DefaultTreeVisitor implements CssCompilerPass { |
| @VisibleForTesting |
| interface MethodByPathHelper { |
| JMethod getMethodByPath(ResourceContext context, List<String> pathElements, |
| JType expectedReturnType) throws NotFoundException; |
| } |
| |
| private static class MethodByPathHelperImpl implements MethodByPathHelper { |
| @Override |
| public JMethod getMethodByPath(ResourceContext context, List<String> pathElements, |
| JType expectedReturnType) throws NotFoundException { |
| return ResourceGeneratorUtil.getMethodByPath(context.getClientBundleType(), |
| pathElements, expectedReturnType); |
| } |
| } |
| |
| private static final String SPRITE_PROPERTY_NAME = "gwt-sprite"; |
| |
| private final MutatingVisitController visitController; |
| private final ErrorManager errorManager; |
| private final ResourceContext context; |
| private final ImageSpriteCreator.MethodByPathHelper methodByPathHelper; |
| private final JClassType imageResourceType; |
| private final String resourceThisPrefix; |
| |
| public ImageSpriteCreator(MutatingVisitController visitController, ResourceContext context, |
| ErrorManager errorManager) { |
| this(visitController, context, errorManager, new MethodByPathHelperImpl()); |
| } |
| |
| @VisibleForTesting |
| ImageSpriteCreator(MutatingVisitController visitController, ResourceContext context, |
| ErrorManager errorManager, MethodByPathHelper methodByPathHelper) { |
| this.visitController = visitController; |
| this.errorManager = errorManager; |
| this.context = context; |
| this.methodByPathHelper = methodByPathHelper; |
| this.imageResourceType = context.getGeneratorContext().getTypeOracle().findType( |
| ImageResource.class.getName()); |
| this.resourceThisPrefix = context.getImplementationSimpleSourceName() + ".this"; |
| } |
| |
| @Override |
| public boolean enterDeclaration(CssDeclarationNode declaration) { |
| String propertyName = declaration.getPropertyName().getPropertyName(); |
| |
| if (SPRITE_PROPERTY_NAME.equals(propertyName)) { |
| createSprite(declaration); |
| return true; |
| } |
| |
| return super.enterDeclaration(declaration); |
| } |
| |
| private void createSprite(CssDeclarationNode declaration) { |
| List<CssValueNode> valuesNodes = declaration.getPropertyValue().getChildren(); |
| |
| if (valuesNodes.size() != 1) { |
| errorManager.report(new GssError(SPRITE_PROPERTY_NAME + " must have exactly one value", |
| declaration.getSourceCodeLocation())); |
| return; |
| } |
| |
| String imageResource = valuesNodes.get(0).getValue(); |
| |
| JMethod imageMethod; |
| try { |
| imageMethod = methodByPathHelper.getMethodByPath(context, getPathElement(imageResource), |
| imageResourceType); |
| } catch (NotFoundException e) { |
| errorManager.report(new GssError("Unable to find ImageResource method " |
| + imageResource + " in " + context.getClientBundleType().getQualifiedSourceName() + " : " |
| + e.getMessage(), declaration.getSourceCodeLocation())); |
| return; |
| } |
| |
| ImageOptions options = imageMethod.getAnnotation(ImageOptions.class); |
| RepeatStyle repeatStyle = options != null ? options.repeatStyle() : RepeatStyle.None; |
| |
| Builder<CssDeclarationNode> listBuilder = ImmutableList.builder(); |
| SourceCodeLocation sourceCodeLocation = declaration.getSourceCodeLocation(); |
| |
| String repeatText; |
| switch (repeatStyle) { |
| case None: |
| repeatText = " no-repeat"; |
| listBuilder.add(buildHeightDeclaration(imageResource, sourceCodeLocation)); |
| listBuilder.add(buildWidthDeclaration(imageResource, sourceCodeLocation)); |
| break; |
| case Horizontal: |
| repeatText = " repeat-x"; |
| listBuilder.add(buildHeightDeclaration(imageResource, sourceCodeLocation)); |
| break; |
| case Vertical: |
| repeatText = " repeat-y"; |
| listBuilder.add(buildWidthDeclaration(imageResource, sourceCodeLocation)); |
| break; |
| case Both: |
| repeatText = " repeat"; |
| break; |
| default: |
| errorManager.report(new GssError("Unknown repeatStyle " + repeatStyle, |
| sourceCodeLocation)); |
| return; |
| } |
| |
| listBuilder.add(buildOverflowDeclaration(sourceCodeLocation)); |
| listBuilder.add(buildBackgroundDeclaration(imageResource, repeatText, sourceCodeLocation)); |
| |
| visitController.replaceCurrentBlockChildWith(listBuilder.build(), false); |
| } |
| |
| private CssDeclarationNode buildBackgroundDeclaration(String imageResource, String repeatText, |
| SourceCodeLocation location) { |
| // build the url function |
| CssFunctionNode urlFunction = new CssFunctionNode(Function.byName("url"), location); |
| CssDotPathNode imageUrl = new CssDotPathNode(resourceThisPrefix, imageResource + ".getSafeUri" + |
| ".asString", null, null, location); |
| CssFunctionArgumentsNode urlFunctionArguments = new CssFunctionArgumentsNode(); |
| urlFunctionArguments.addChildToBack(imageUrl); |
| urlFunction.setArguments(urlFunctionArguments); |
| |
| // build left offset |
| CssDotPathNode left = new CssDotPathNode(resourceThisPrefix, imageResource + ".getLeft", "-", |
| "px", location); |
| |
| // build top offset |
| CssDotPathNode top = new CssDotPathNode(resourceThisPrefix, imageResource + ".getTop", |
| "-", "px", location); |
| |
| // build repeat |
| CssLiteralNode repeat = new CssLiteralNode(repeatText, location); |
| |
| CssPropertyNode propertyNode = new CssPropertyNode("background", location); |
| CssPropertyValueNode propertyValueNode = new CssPropertyValueNode(ImmutableList.of(urlFunction, |
| left, top, repeat)); |
| propertyValueNode.setSourceCodeLocation(location); |
| |
| return createDeclarationNode(propertyNode, propertyValueNode, location, true); |
| } |
| |
| private CssDeclarationNode buildHeightDeclaration(String imageResource, |
| SourceCodeLocation location) { |
| CssPropertyNode propertyNode = new CssPropertyNode("height", location); |
| CssValueNode valueNode = new CssDotPathNode(resourceThisPrefix, imageResource + ".getHeight", |
| null, "px", location); |
| |
| CssPropertyValueNode propertyValueNode = new CssPropertyValueNode(ImmutableList.of(valueNode)); |
| |
| return createDeclarationNode(propertyNode, propertyValueNode, location, true); |
| } |
| |
| private CssDeclarationNode buildOverflowDeclaration(SourceCodeLocation location) { |
| CssPropertyNode propertyNode = new CssPropertyNode("overflow", location); |
| CssValueNode valueNode = new CssLiteralNode("hidden", location); |
| |
| CssPropertyValueNode propertyValueNode = new CssPropertyValueNode(ImmutableList.of(valueNode)); |
| |
| return createDeclarationNode(propertyNode, propertyValueNode, location, true); |
| } |
| |
| private CssDeclarationNode buildWidthDeclaration(String imageResource, |
| SourceCodeLocation location) { |
| CssPropertyNode propertyNode = new CssPropertyNode("width", location); |
| CssValueNode valueNode = new CssDotPathNode(resourceThisPrefix, imageResource + ".getWidth", |
| null, "px", location); |
| CssPropertyValueNode propertyValueNode = new CssPropertyValueNode(ImmutableList.of(valueNode)); |
| |
| return createDeclarationNode(propertyNode, propertyValueNode, location, true); |
| } |
| |
| private List<String> getPathElement(String imageResourcePath) { |
| return Lists.newArrayList(imageResourcePath.split("\\.")); |
| } |
| |
| private CssDeclarationNode createDeclarationNode(CssPropertyNode propertyNode, |
| CssPropertyValueNode propertyValueNode, SourceCodeLocation location, boolean useAlternate) { |
| CssDeclarationNode replaceNode = new CssDeclarationNode(propertyNode, propertyValueNode); |
| replaceNode.setSourceCodeLocation(location); |
| |
| if (useAlternate) { |
| replaceNode.setComments(ImmutableList.of(new CssCommentNode(ALTERNATE, location))); |
| } |
| |
| return replaceNode; |
| } |
| |
| @Override |
| public void runPass() { |
| visitController.startVisit(this); |
| } |
| } |