com.google.common.css.compiler.gssfunctions.GssFunctions.java Source code

Java tutorial

Introduction

Here is the source code for com.google.common.css.compiler.gssfunctions.GssFunctions.java

Source

/*
 * Copyright 2010 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.common.css.compiler.gssfunctions;

import static com.google.common.css.compiler.gssfunctions.ColorUtil.formatColor;
import static com.google.common.css.compiler.gssfunctions.ColorUtil.hsbToColor;
import static com.google.common.css.compiler.gssfunctions.ColorUtil.testContrast;
import static com.google.common.css.compiler.gssfunctions.ColorUtil.toHsb;

import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.css.SourceCodeLocation;
import com.google.common.css.compiler.ast.CssCompositeValueNode;
import com.google.common.css.compiler.ast.CssFunctionArgumentsNode;
import com.google.common.css.compiler.ast.CssFunctionNode;
import com.google.common.css.compiler.ast.CssHexColorNode;
import com.google.common.css.compiler.ast.CssLiteralNode;
import com.google.common.css.compiler.ast.CssNumericNode;
import com.google.common.css.compiler.ast.CssValueNode;
import com.google.common.css.compiler.ast.ErrorManager;
import com.google.common.css.compiler.ast.GssError;
import com.google.common.css.compiler.ast.GssFunction;
import com.google.common.css.compiler.ast.GssFunctionException;

import java.awt.Color;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import javax.annotation.Nullable;

/**
 * Container for common GSS functions.
 *
 * @author oana@google.com (Oana Florescu)
 * @author dgajda@google.com (Damian Gajda)
 */
public class GssFunctions {

    /**
     * @return a map from each GSS function name to the function
     */
    public static Map<String, GssFunction> getFunctionMap() {
        // TODO(dgajda): Add getName() to the function interface.
        return ImmutableMap.<String, GssFunction>builder()

                // Arithmetic functions.
                .put("add", new GssFunctions.AddToNumericValue())
                .put("sub", new GssFunctions.SubtractFromNumericValue()).put("mult", new GssFunctions.Mult())
                // Not named "div" so it will not be confused with the HTML element.
                .put("divide", new GssFunctions.Div()).put("min", new GssFunctions.MinValue())
                .put("max", new GssFunctions.MaxValue())

                // Color functions.
                .put("blendColorsHsb", new BlendColorsHsb()).put("blendColorsRgb", new BlendColorsRgb())
                .put("makeMutedColor", new MakeMutedColor()).put("addHsbToCssColor", new AddHsbToCssColor())
                .put("makeContrastingColor", new MakeContrastingColor())
                .put("adjustBrightness", new AdjustBrightness()).put("makeTranslucent", new MakeTranslucent())

                // Logic functions.
                .put("selectFrom", new SelectFrom())

                .build();
    }

    /**
     * Round decimals to the eight places, which appears to be the smallest
     * precision that works well across all browsers. (Yes, this is crazy.)
     */
    private static final String DECIMAL_FORMAT = "#.########";

    /**
     * This class encapsulates results of background definition calculation and
     * is used to build either a list of {@link CssValueNode} instances or a string that
     * represents the background CSS property.
     */
    static class ImageBackground {

        private static final String NO_REPEAT = "no-repeat";

        private final String url;
        private final String positionH;
        private final String positionHUnit;
        private final String positionV;
        private final String positionVUnit;

        /**
         * @param url The URL to be used as the background image URL
         * @param cornerId The corner id which tells how the image is positioned
         * @param imgSize The size of the image
         * @param units The units of the image size
         */
        public ImageBackground(String url, String cornerId, String imgSize, String units) {
            this.url = url;

            boolean isZero = Float.parseFloat(imgSize) == 0;

            boolean isLeft = isZero || cornerId.endsWith("l");
            positionH = isLeft ? "0" : "-" + imgSize;
            positionHUnit = isLeft ? CssNumericNode.NO_UNITS : units;

            boolean isTop = isZero || cornerId.startsWith("t");
            positionV = isTop ? "0" : "-" + imgSize;
            positionVUnit = isTop ? CssNumericNode.NO_UNITS : units;
        }

        @Override
        public String toString() {
            return createUrl(url) + " " + NO_REPEAT + " " + positionH + positionHUnit + " " + positionV
                    + positionVUnit;
        }

        public List<CssValueNode> toNodes(SourceCodeLocation location) {
            return ImmutableList.of(createUrlNode(url, location), new CssLiteralNode(NO_REPEAT, location),
                    new CssNumericNode(positionH, positionHUnit, location),
                    new CssNumericNode(positionV, positionVUnit, location));
        }
    }

    /**
     * Base implementation of the color blending GSS function. Returns a color
     * half way between the two colors supplied as arguments.
     */
    public abstract static class BaseBlendColors implements GssFunction {

        /**
         * Returns the number of expected arguments of this GSS function.
         *
         * @return Number of expected arguments
         */
        @Override
        public Integer getNumExpectedArguments() {
            return 2;
        }

        /**
         * Returns the string representation in hex format for a color half way in
         * between the two supplied colors.
         *
         * @param args The list of arguments
         * @return The computed color
         */
        @Override
        public List<CssValueNode> getCallResultNodes(List<CssValueNode> args, ErrorManager errorManager) {

            CssValueNode arg1 = args.get(0);
            CssValueNode arg2 = args.get(1);

            String startColorStr = arg1.getValue();
            String endColorStr = arg2.getValue();

            String resultString = blend(startColorStr, endColorStr);

            CssHexColorNode result = new CssHexColorNode(resultString, arg1.getSourceCodeLocation());
            return ImmutableList.of((CssValueNode) result);
        }

        @Override
        public String getCallResultString(List<String> args) throws GssFunctionException {
            try {
                return blend(args.get(0), args.get(1));
            } catch (IllegalArgumentException e) {
                throw new GssFunctionException("Colors could not be parsed", e);
            }
        }

        // TODO(dgajda): Hide it, this function is only visible because
        public abstract String blend(String startColor, String endColor);
    }

    /**
     * Implementation of the blendColorsHsb GSS function. Returns a color half way
     * between the two colors supplied as arguments.
     */
    public static class BlendColorsHsb extends BaseBlendColors {

        @Override
        // TODO(dgajda): Hide it, this function is only visible because
        public String blend(String startColorStr, String endColorStr) {
            Color midColor = blendHsb(ColorParser.parseAny(startColorStr), ColorParser.parseAny(endColorStr));
            return formatColor(midColor);
        }
    }

    private static Color blendHsb(Color startColor, Color endColor) {

        float[] startColorHsb = toHsb(startColor);
        float[] endColorHsb = toHsb(endColor);

        float diffHue = Math.abs(startColorHsb[0] - endColorHsb[0]);
        float sumHue = startColorHsb[0] + endColorHsb[0];
        float midHue = (diffHue <= 0.5) ? sumHue / 2 : (sumHue + 1) / 2; // Hue values range 0 to 1 and wrap (i.e. 0 == 1)
        if (midHue > 1) {
            midHue -= 1;
        }

        return Color.getHSBColor(midHue, (startColorHsb[1] + endColorHsb[1]) / 2,
                (startColorHsb[2] + endColorHsb[2]) / 2);
    }

    /**
     * Implementation of the blendColorsRgb GSS function. Returns a color half way
     * between the two colors by averaging each of red, green & blue.
     */
    public static class BlendColorsRgb extends BaseBlendColors {

        /**
         * Returns the string representation in hex format for a color half way in
         * between the two supplied colors by averaging each of red, green & blue.
         *
         * @param startColorStr The start color in string form
         * @param endColorStr The endcolor in string form
         * @return The computed color
         */
        @Override
        // TODO(dgajda): Hide it, this function is only visible because
        public String blend(String startColorStr, String endColorStr) {
            Color startColor = ColorParser.parseAny(startColorStr);
            Color endColor = ColorParser.parseAny(endColorStr);

            Color midColor = new Color((startColor.getRed() + endColor.getRed()) / 2,
                    (startColor.getGreen() + endColor.getGreen()) / 2,
                    (startColor.getBlue() + endColor.getBlue()) / 2);

            return formatColor(midColor);
        }
    }

    /**
     * Helper method to convert a numeric value of "0" or "1" into a boolean.
     *
     * @param numericPart The string containing the value
     * @return The corresponding boolean value
     */
    public static boolean parseBoolean(String numericPart) {
        return Integer.parseInt(numericPart) == 1;
    }

    /**
     * Helper method for implementors of GssFunction to allow the creation of
     * a url entry node in a GSS file.
     *
     * @param imageUrl The url of the image to add.
     * @param location The location in the GSS file to place the node.
     * @return The node containing the url entry.
     */
    public static CssFunctionNode createUrlNode(String imageUrl, SourceCodeLocation location) {
        CssFunctionNode url = new CssFunctionNode(CssFunctionNode.Function.byName("url"), location);

        if (!imageUrl.equals("")) {
            CssLiteralNode argument = new CssLiteralNode(imageUrl, location);
            List<CssValueNode> argList = ImmutableList.of((CssValueNode) argument);
            CssFunctionArgumentsNode arguments = new CssFunctionArgumentsNode(argList);
            url.setArguments(arguments);
        }
        return url;
    }

    /**
     * Implementation of the addHsbToCssColor GSS function.
     */
    public static class AddHsbToCssColor implements GssFunction {

        @Override
        public Integer getNumExpectedArguments() {
            return 4;
        }

        @Override
        public List<CssValueNode> getCallResultNodes(List<CssValueNode> args, ErrorManager errorManager)
                throws GssFunctionException {
            CssValueNode arg1 = args.get(0);
            CssValueNode arg2 = args.get(1);
            CssValueNode arg3 = args.get(2);
            CssValueNode arg4 = args.get(3);

            if (!(arg1 instanceof CssHexColorNode || arg1 instanceof CssLiteralNode)) {
                String message = "The first argument must be a CssHexColorNode or a CssLiteralNode.";
                errorManager.report(new GssError(message, arg1.getSourceCodeLocation()));
                throw new GssFunctionException(message);
            }
            CssNumericNode numeric2, numeric3, numeric4;
            if (arg2 instanceof CssNumericNode && arg3 instanceof CssNumericNode
                    && arg4 instanceof CssNumericNode) {
                numeric2 = (CssNumericNode) arg2;
                numeric3 = (CssNumericNode) arg3;
                numeric4 = (CssNumericNode) arg4;
            } else {
                String message = "Arguments number 2, 3 and 4 must be CssNumericNodes";
                errorManager.report(new GssError(message, arg2.getSourceCodeLocation()));
                throw new GssFunctionException(message);
            }

            try {
                String resultString = addHsbToCssColor(args.get(0).getValue(), numeric2.getNumericPart(),
                        numeric3.getNumericPart(), numeric4.getNumericPart());

                CssHexColorNode result = new CssHexColorNode(resultString, arg1.getSourceCodeLocation());
                return ImmutableList.of((CssValueNode) result);
            } catch (GssFunctionException e) {
                errorManager.report(new GssError(e.getMessage(), arg2.getSourceCodeLocation()));
                throw e;
            }
        }

        @Override
        public String getCallResultString(List<String> args) throws GssFunctionException {
            String baseColorString = args.get(0);
            return addHsbToCssColor(baseColorString, args.get(1), args.get(2), args.get(3));
        }

        protected String addHsbToCssColor(String baseColorString, String hueToAdd, String saturationToAdd,
                String brightnessToAdd) throws GssFunctionException {
            try {
                return addHsbToCssColor(baseColorString, Integer.parseInt(hueToAdd),
                        Integer.parseInt(saturationToAdd), Integer.parseInt(brightnessToAdd));
            } catch (NumberFormatException e) {
                String message = String.format("Could not parse the integer arguments"
                        + " for the function 'addHsbToCssColor'. The list of arguments was:" + " %s, %s, %s, %s. ",
                        baseColorString, hueToAdd, saturationToAdd, brightnessToAdd);
                throw new GssFunctionException(message);
            } catch (IllegalArgumentException e) {
                String message = String.format("Could not parse the color argument"
                        + " for the function 'addHsbToCssColor'. The list of arguments was:" + " %s, %s, %s, %s. ",
                        baseColorString, hueToAdd, saturationToAdd, brightnessToAdd);
                throw new GssFunctionException(message);
            }
        }

        /**
         * Takes a CSS color string, and adds the specified amount of hue,
         * saturation and brightness to it.
         *
         * @param baseColorString The string representing the color to change
         * @param hueToAdd The amount of hue to add (can be negative)
         * @param saturationToAdd The amount of saturation to add (can be negative)
         * @param brightnessToAdd The amount of brightness to add (can be negative)
         * @return A CSS String representing the new color
         */
        public String addHsbToCssColor(String baseColorString, int hueToAdd, int saturationToAdd,
                int brightnessToAdd) {

            // Skip transformation for the transparent color.
            if ("transparent".equals(baseColorString)) {
                return baseColorString;
            }

            Color baseColor = ColorParser.parseAny(baseColorString);
            Color newColor = addValuesToHsbComponents(baseColor, hueToAdd, saturationToAdd, brightnessToAdd);

            return formatColor(newColor);
        }

        /**
         * Adds the specified amount to the specified HSB (Hue, Saturation,
         * Brightness) parameter of the given color. The amount can be negative.
         *
         * @param baseColor The color to modify
         * @param hueToAdd The amount of hue to add
         * @param saturationToAdd The amount of saturation to add
         * @param brightnessToAdd The amount of brightness to add
         * @return The modified color
         */
        public Color addValuesToHsbComponents(Color baseColor, int hueToAdd, int saturationToAdd,
                int brightnessToAdd) {

            float[] hsbValues = toHsb(baseColor);

            // In HSB color space, Hue goes from 0 to 360, Saturation and Brightness
            // from 0 to 100. However, in Java all three parameters vary from 0.0 to
            // 1.0, so we need some basic conversion.
            hsbValues[0] = (float) (hsbValues[0] + hueToAdd / 360.0);
            // The hue needs to wrap around, so just keep hue - floor(hue).
            hsbValues[0] -= (float) Math.floor(hsbValues[0]);

            // For saturation and brightness, no wrapping around, we just make sure
            // we don't go over 1.0 or under 0.0
            hsbValues[1] = (float) Math.min(1.0, Math.max(0, hsbValues[1] + saturationToAdd / 100.0));
            hsbValues[2] = (float) Math.min(1.0, Math.max(0, hsbValues[2] + brightnessToAdd / 100.0));

            return Color.getHSBColor(hsbValues[0], hsbValues[1], hsbValues[2]);
        }
    }

    /**
     * Implementation of the makeMutedColor GSS function. This is intended to
     * generate a muted flavor of a text or link color. Takes three arguments: the
     * background color over which this text or link will appear, and the text or
     * link color this should be a muted version of and optionally the loss of
     * saturation for muted tone (0 <= loss <= 1).
     */
    public static class MakeMutedColor implements GssFunction {

        private float LOSS_OF_SATURATION_FOR_MUTED_TONE = 0.2f;
        private String ARGUMENT_COUNT_ERROR_MESSAGE = "makeMutedColor "
                + "expected arguments: backgroundColorStr, foregroundColorStr and an "
                + "optional loss of saturation value (0 <= loss <= 1).";

        /**
         * Returns the number of expected arguments of this GSS function.
         *
         * @return Number of expected arguments
         */
        @Override
        public Integer getNumExpectedArguments() {
            return null;
        }

        /**
         * Returns the muted color corresponding to the arguments
         * documented in {@link MakeMutedColor}.
         *
         * @param args The list of arguments.
         * @return The generated muted color.
         */
        @Override
        public List<CssValueNode> getCallResultNodes(List<CssValueNode> args, ErrorManager errorManager)
                throws GssFunctionException {

            if (args.size() != 2 && args.size() != 3) {
                throw new GssFunctionException(ARGUMENT_COUNT_ERROR_MESSAGE);
            }

            CssValueNode backgroundColorNode = args.get(0);
            CssValueNode foregroundColorNode = args.get(1);
            String lossOfSaturationForMutedTone = String.valueOf(LOSS_OF_SATURATION_FOR_MUTED_TONE);

            if (args.size() == 3) {
                lossOfSaturationForMutedTone = args.get(2).getValue();
            }

            String backgroundColorStr = backgroundColorNode.getValue();
            String foregroundColorStr = foregroundColorNode.getValue();

            String resultStr = makeMutedColor(backgroundColorStr, foregroundColorStr, lossOfSaturationForMutedTone);

            CssHexColorNode result = new CssHexColorNode(resultStr, backgroundColorNode.getSourceCodeLocation());
            return ImmutableList.of((CssValueNode) result);
        }

        protected String makeMutedColor(String backgroundColorStr, String foregroundColorStr, String lossStr) {

            // If the background is transparent, or if the foreground is transparent,
            // there's really no way we can know how to pick a muted color. We thus
            // return the foreground color unchanged.
            if ("transparent".equalsIgnoreCase(backgroundColorStr)
                    || "transparent".equalsIgnoreCase(foregroundColorStr)) {
                return foregroundColorStr;
            }
            Color backgroundColor = ColorParser.parseAny(backgroundColorStr);
            Color foregroundColor = ColorParser.parseAny(foregroundColorStr);

            float[] backgroundColorHsb = toHsb(backgroundColor);
            float[] foregroundColorHsb = toHsb(foregroundColor);
            float lossOfSaturationForMutedTone = Float.valueOf(lossStr);

            // Make sure that 0 <= lossOfSaturationForMutedTone <= 1
            if (lossOfSaturationForMutedTone < 0) {
                lossOfSaturationForMutedTone = 0;
            } else if (lossOfSaturationForMutedTone > 1) {
                lossOfSaturationForMutedTone = 1;
            }

            // We take the hue from the foreground color, we desaturate it a little
            // bit, and choose a brightness halfway between foreground and background.
            // For example, if the background has a brightness of 50, and 100 for the
            // foreground, the muted color will have 75. If we have a dark background,
            // it should be the reverse.
            float mutedHue = foregroundColorHsb[0];
            float mutedSaturation = Math.max(foregroundColorHsb[1] - lossOfSaturationForMutedTone, 0);
            float mutedBrightness = (foregroundColorHsb[2] + backgroundColorHsb[2]) / 2;

            Color mutedColor = Color.getHSBColor(mutedHue, mutedSaturation, mutedBrightness);

            return formatColor(mutedColor);
        }

        protected String makeMutedColor(String backgroundColorStr, String foregroundColorStr) {
            return makeMutedColor(backgroundColorStr, foregroundColorStr,
                    String.valueOf(LOSS_OF_SATURATION_FOR_MUTED_TONE));
        }

        @Override
        public String getCallResultString(List<String> args) throws GssFunctionException {
            if (args.size() == 2) {
                return makeMutedColor(args.get(0), args.get(1));
            } else if (args.size() == 3) {
                return makeMutedColor(args.get(0), args.get(1), args.get(2));
            } else {
                throw new GssFunctionException(ARGUMENT_COUNT_ERROR_MESSAGE);
            }
        }
    }

    /**
     * Abstract class implementing the shared logic for the arithmetic functions.
     */
    private static abstract class LeftAssociativeOperator implements GssFunction {

        /**
         * Returns the number of expected arguments of this GSS function.
         *
         * @return Number of expected arguments
         */
        @Override
        public Integer getNumExpectedArguments() {
            // Takes a variable number of arguments.
            return null;
        }

        /**
         * Returns a new value of the same unit as the original.
         *
         * @param args The list of arguments
         * @return The resulting fingerprint as a numeric node in a list
         */
        @Override
        public List<CssValueNode> getCallResultNodes(List<CssValueNode> args, ErrorManager errorManager)
                throws GssFunctionException {
            List<CssNumericNode> numericList = Lists.newArrayList();
            for (CssValueNode arg : args) {
                numericList.add(getSizeNode(arg, errorManager, true /* isUnitOptional */));
            }
            return ImmutableList.<CssValueNode>of(calculate(numericList, errorManager));
        }

        @Override
        public String getCallResultString(List<String> args) throws GssFunctionException {
            List<CssNumericNode> numericList = Lists.newArrayList();
            for (String arg : args) {
                // Note, the unit may be 'NO_UNITS'
                Size sizeWithUnits = parseSize(arg, true /* isUnitOptional */);
                numericList.add(new CssNumericNode(sizeWithUnits.size, sizeWithUnits.units));
            }
            CssNumericNode result = calculate(numericList, null);
            return result.getNumericPart() + result.getUnit();
        }

        // Note: Keep an eye on the performance of these functions, as creating
        // intermediate CssNumericNodes may be wasteful. Instead, the values in the
        // nodes could be used directly for computation, though that may make
        // accurate error reporting more difficult.

        protected CssNumericNode calculate(List<CssNumericNode> args, ErrorManager errorManager)
                throws GssFunctionException {
            if (args.size() < 2) {
                throw error("Not enough arguments", errorManager, args.get(0).getSourceCodeLocation());
            }

            double total = Double.valueOf(args.get(0).getNumericPart());
            String overallUnit = isIdentityValue(total) ? null : args.get(0).getUnit();

            for (CssNumericNode node : args.subList(1, args.size())) {
                double value = Double.valueOf(node.getNumericPart());
                if (isIdentityValue(value)) {
                    continue;
                }
                if (overallUnit == null) {
                    overallUnit = node.getUnit();
                } else if (!overallUnit.equals(node.getUnit())) {
                    throw error(
                            "Parameters' units don't match (\"" + overallUnit + "\" vs \"" + node.getUnit() + "\")",
                            errorManager, node.getSourceCodeLocation());
                }
                total = performOperation(total, value);
            }
            String resultString = new DecimalFormat(DECIMAL_FORMAT).format(total);

            return new CssNumericNode(resultString, overallUnit != null ? overallUnit : CssNumericNode.NO_UNITS,
                    args.get(0).getSourceCodeLocation());
        }

        // Perform the mathematical operation.
        protected abstract double performOperation(double left, double right);

        /**
         * By default, this method returns {@code false}.
         * @return whether the identity value has no effect on the output. In this
         *     case, any units (px, pt, etc.) will be ignored.
         */
        protected boolean isIdentityValue(double value) {
            return false;
        }
    }

    /**
     * The "add()" function adds a list of numeric values.
     */
    public static class AddToNumericValue extends LeftAssociativeOperator {
        @Override
        protected double performOperation(double left, double right) {
            return left + right;
        }

        @Override
        protected boolean isIdentityValue(double value) {
            return value == 0.0;
        }
    }

    /**
     * The "sub()" function subtracts a list of numeric values.
     * SubtractFromNumericValue(a, b, c) evaluates to ((a - b) - c).
     */
    public static class SubtractFromNumericValue extends LeftAssociativeOperator {
        @Override
        protected double performOperation(double left, double right) {
            return left - right;
        }

        @Override
        protected boolean isIdentityValue(double value) {
            return value == 0.0;
        }
    }

    /**
     * A {@link GssFunction} that returns the max value from its list of
     * arguments.
     */
    public static class MaxValue extends LeftAssociativeOperator {
        @Override
        protected double performOperation(double left, double right) {
            return Math.max(left, right);
        }
    }

    /**
     * A {@link GssFunction} that returns the min value from its list of
     * arguments.
     */
    public static class MinValue extends LeftAssociativeOperator {
        @Override
        protected double performOperation(double left, double right) {
            return Math.min(left, right);
        }
    }

    /**
     * A {@link ScalarLeftAssociativeOperator} is a left associative operator
     * whose arguments are all scalars, with the possible exception of the first
     * argument, which may be a {@link Size} rather than a scalar.
     */
    private static abstract class ScalarLeftAssociativeOperator extends LeftAssociativeOperator {

        @Override
        protected CssNumericNode calculate(List<CssNumericNode> args, ErrorManager errorManager)
                throws GssFunctionException {
            if (args.size() == 0) {
                throw error("Not enough arguments", errorManager, args.get(0).getSourceCodeLocation());
            }

            double total = Double.valueOf(args.get(0).getNumericPart());
            String overallUnit = args.get(0).getUnit();

            for (CssNumericNode node : args.subList(1, args.size())) {
                if (node.getUnit() != null && !node.getUnit().equals(CssNumericNode.NO_UNITS)) {
                    throw error("Only the first argument may have a unit associated with it, " + " but has unit: "
                            + node.getUnit(), errorManager, node.getSourceCodeLocation());
                }

                double value = Double.valueOf(node.getNumericPart());
                total = performOperation(total, value);
            }
            String resultString = new DecimalFormat(DECIMAL_FORMAT).format(total);

            return new CssNumericNode(resultString, overallUnit != null ? overallUnit : CssNumericNode.NO_UNITS,
                    args.get(0).getSourceCodeLocation());
        }
    }

    /**
     * A {@link GssFunction} that returns the product of its arguments. Only the
     * first argument may have a unit.
     */
    public static class Mult extends ScalarLeftAssociativeOperator {
        @Override
        protected double performOperation(double left, double right) {
            return left * right;
        }

        @Override
        protected boolean isIdentityValue(double value) {
            return value == 1.0;
        }
    }

    /**
     * A {@link GssFunction} that returns the quotient of its arguments. Only the
     * first argument may have a unit.
     */
    public static class Div extends ScalarLeftAssociativeOperator {
        @Override
        protected double performOperation(double left, double right) {
            return left / right;
        }

        @Override
        protected boolean isIdentityValue(double value) {
            return value == 1.0;
        }
    }

    /**
     * Implementation of the adjustBrightness GSS function. This generates
     * a slightly differentiated color suitable for hover styling. Takes the
     * color to modify as the first argument, and the requested brightness
     * difference as a second argument (between 0 and 100). This function will
     * always ensure that the returned color is different from the input, e.g.
     * attempting to "brighten" white will return a light grey instead of white,
     * but if it isn't possible to find a color that matches the request
     * difference (e.g. asking to brighten by 100 from a medium bright color),
     * the returned value will be a color with a difference from the input color
     * as close as possible to what is being requested. See the unit test for
     * some examples.
     */
    public static class AdjustBrightness implements GssFunction {

        /**
         * Returns the number of expected arguments of this GSS function.
         *
         * @return Number of expected arguments
         */
        @Override
        public Integer getNumExpectedArguments() {
            return 2;
        }

        /**
         * Returns the hover color corresponding to the argument
         * documented in {@link AdjustBrightness}.
         *
         * @param args The list of arguments.
         * @return The generated hover color.
         */
        @Override
        public List<CssValueNode> getCallResultNodes(List<CssValueNode> args, ErrorManager errorManager) {

            CssValueNode originalColorNode = args.get(0);
            CssValueNode brightnessAmount = args.get(1);

            String originalColorStr = originalColorNode.getValue();
            String brightnessAmountStr = ((CssNumericNode) brightnessAmount).getNumericPart();

            String resultStr = adjustBrightness(originalColorStr, brightnessAmountStr);

            CssHexColorNode result = new CssHexColorNode(resultStr, originalColorNode.getSourceCodeLocation());
            return ImmutableList.of((CssValueNode) result);
        }

        private float normalize(float value) {
            if (value > 1.0) {
                return 1;
            }
            if (value < 0.0) {
                return 0;
            }
            return value;
        }

        private String formatColorWithAdjustedBrightness(float[] originalHsb, float adjustedBrightness) {
            return formatColor(Color.getHSBColor(originalHsb[0], originalHsb[1], adjustedBrightness));
        }

        protected String adjustBrightness(String originalColorStr, String brightnessStr) {

            // If the input color is transparent, there's really no way we can know
            // how to pick a good output color. We thus return the color unchanged.
            if ("transparent".equalsIgnoreCase(originalColorStr)) {
                return originalColorStr;
            }
            Color originalColor = ColorParser.parseAny(originalColorStr);
            float brightnessFloat = Float.parseFloat(brightnessStr) / (float) 100.0;

            float[] originalColorHsb = toHsb(originalColor);
            float requestedBrightness = originalColorHsb[2] + brightnessFloat;

            // If we're not "saturating" to white or black, then we can meet
            // exactly what the caller requests.
            if (requestedBrightness >= 0.0 && requestedBrightness <= 1.0) {
                return formatColorWithAdjustedBrightness(originalColorHsb, requestedBrightness);
            }

            // If we can't get exactly what's requested, we try both directions to
            // be as close as possible to the requested brightness difference.
            requestedBrightness = normalize(requestedBrightness);
            float oppositeDirectionBrightness = normalize(originalColorHsb[2] - brightnessFloat);

            // Calculate the distance between what the caller requests and the two
            // possibilites we have, then return the closest.
            float chosenBrightness = Math.abs(brightnessFloat - (originalColorHsb[2] - requestedBrightness)) > Math
                    .abs(brightnessFloat - (originalColorHsb[2] - oppositeDirectionBrightness))
                            ? oppositeDirectionBrightness
                            : requestedBrightness;
            return formatColorWithAdjustedBrightness(originalColorHsb, chosenBrightness);
        }

        @Override
        public String getCallResultString(List<String> args) {
            return adjustBrightness(args.get(0), args.get(1));
        }
    }

    /**
     * The "makeContrastingColor" function generates a contrasting color with the
     * same hue as the input color.  The generated color passes (or almost
     * passes) the contrast test described in the W3C accessibility evaluation
     * working draft {@link "http://www.w3.org/TR/AERT#color-contrast"}.
     *
     * <p>The function takes two parameters:
     * <ul>
     * <li>The input color to find the contrasting color for,
     * <li>the similarity value (a float between 0.0 - 1.0) which tells how
     *   similar the computed color should be to the input color.
     * </ul>
     *
     * <p>The algorithm for the contrasting color generation is as follows:
     * <ol>
     * <li>First a base contrasting color is chosen.  It is either black or
     *   white, as one of these colors is guaranteed to be in contrast with any
     *   given color.  Additionally both of these colors are "hue-neutral", so
     *   choosing the contrasting color in between the input color and black or
     *   white should give pleasant results.
     *
     * <li>A closest required contrasting color is found.  This is a color in
     *   between the input color and the base contrasting color.  It has the same
     *   hue as the input color.  This color is found in HSB color space, by a
     *   bisection of a line between the input color and the base contrasting
     *   color.  To improve function performance, a limited number of the
     *   bisection steps ({@link MakeContrastingColor#NUM_ITERATIONS}) is
     *   performed.  This gives a constant expected time of function execution.
     *   At the same time the color that is found may not pass the contrast test,
     *   but is guaranteed to be close to the "contrast boundary".
     *
     * <li>The output color of the function is an interpolation of the closest
     *   contrasting color and the base contrasting color.  The interpolation is
     *   controlled using the "similarity" parameter.  If similarity is set to 1,
     *   it means that the result is the closest contrasting color.  If
     *   similarity is set to 0, it means that the result is the base contrasting
     *   color.
     * </ol>
     */
    public static class MakeContrastingColor implements GssFunction {

        /**
         * Number of iterations to approximate the closest contrasting color.
         * It is set to a number which should be enough to converge in the 8-bit
         * color component space we deal with. Since number of iterations is fixed
         * function computation time is also fixed.
         */
        private static final int NUM_ITERATIONS = 8;

        @Override
        public Integer getNumExpectedArguments() {
            return 2;
        }

        @Override
        public List<CssValueNode> getCallResultNodes(List<CssValueNode> args, ErrorManager errorManager) {
            CssValueNode arg1 = args.get(0);
            CssValueNode arg2 = args.get(1);

            // TODO(dgajda): We should check the type of the color node.
            String color = arg1.getValue();
            String similarity = arg2.toString();

            String resultStr = makeContrastingColor(color, similarity);

            CssHexColorNode result = new CssHexColorNode(resultStr, arg1.getSourceCodeLocation());
            return ImmutableList.of((CssValueNode) result);
        }

        @Override
        public String getCallResultString(List<String> args) {
            return makeContrastingColor(args.get(0), args.get(1));
        }

        protected String makeContrastingColor(String inputColorStr, String similarityStr) {
            if ("transparent".equalsIgnoreCase(inputColorStr)) {
                return inputColorStr;
            }
            Color inputColor = ColorParser.parseAny(inputColorStr);
            float similarity = Float.parseFloat(similarityStr);

            float[] distantColor = toHsb(getDistantColor(inputColor, Color.BLACK, Color.WHITE));

            float[] startColor = toHsb(inputColor);
            float[] endColor = distantColor;
            float[] closestContrastColor = null;

            for (int i = 0; i < NUM_ITERATIONS; i++) {
                closestContrastColor = blendSb(startColor, endColor, 0.5f);
                if (testContrast(inputColor, hsbToColor(closestContrastColor))) {
                    endColor = closestContrastColor;
                } else {
                    startColor = closestContrastColor;
                }
            }

            float[] resultColor = blendSb(closestContrastColor, distantColor, similarity);
            return formatColor(hsbToColor(resultColor));
        }

        private Color getDistantColor(Color color, Color first, Color second) {
            int firstLuminanceDiff = ColorUtil.luminanceDiff(color, first);
            int secondLuminanceDiff = ColorUtil.luminanceDiff(color, second);
            return firstLuminanceDiff >= secondLuminanceDiff ? first : second;
        }

        private float[] blendSb(float[] keepHue, float[] other, float similarity) {
            float[] result = Arrays.copyOf(keepHue, keepHue.length);
            mix(ColorUtil.S, keepHue, other, similarity, result);
            mix(ColorUtil.B, keepHue, other, similarity, result);
            return result;
        }

        private void mix(int componentIdx, float[] sourceHsb, float[] otherColorHsb, float sourceSimilarity,
                float[] resultHsb) {
            resultHsb[componentIdx] = sourceHsb[componentIdx] * sourceSimilarity
                    + otherColorHsb[componentIdx] * (1f - sourceSimilarity);
        }
    }

    /**
     * Takes an input color and sets its alpha component without affecting
     * the RGB components.
     * Usage: makeTranslucent(existingColor, alphaValue);
     */
    public static class MakeTranslucent implements GssFunction {
        @Override
        public Integer getNumExpectedArguments() {
            return 2;
        }

        @Override
        public List<CssValueNode> getCallResultNodes(List<CssValueNode> args, ErrorManager errorManager) {
            CssValueNode arg1 = args.get(0);
            CssValueNode arg2 = args.get(1);

            String color = arg1.getValue();
            String alpha = arg2.toString();

            return ImmutableList.of(makeTranslucent(color, alpha, arg1.getSourceCodeLocation()));
        }

        @Override
        public String getCallResultString(List<String> args) {
            return makeTranslucent(args.get(0), args.get(1), null).getValue();
        }

        protected CssValueNode makeTranslucent(String inputColorStr, String alphaStr,
                @Nullable SourceCodeLocation sourceCodeLocation) {
            Color inputColor = ColorParser.parseAny(inputColorStr);
            double alpha = Math.min(1.0, Math.max(0, Float.parseFloat(alphaStr)));

            float[] rgb = inputColor.getRGBColorComponents(null);
            Color outputColor = new Color(rgb[0], rgb[1], rgb[2], (float) alpha);

            List<CssValueNode> argList = ImmutableList.<CssValueNode>of(
                    new CssLiteralNode(Integer.toString(outputColor.getRed()), sourceCodeLocation),
                    new CssLiteralNode(Integer.toString(outputColor.getGreen()), sourceCodeLocation),
                    new CssLiteralNode(Integer.toString(outputColor.getBlue()), sourceCodeLocation),
                    new CssLiteralNode(new DecimalFormat("#.###").format(outputColor.getAlpha() / 255f),
                            sourceCodeLocation));
            CssValueNode argsValue = new CssCompositeValueNode(argList, CssCompositeValueNode.Operator.COMMA,
                    sourceCodeLocation);
            CssFunctionNode result = new CssFunctionNode(CssFunctionNode.Function.byName("rgba"),
                    sourceCodeLocation);
            result.setArguments(new CssFunctionArgumentsNode(ImmutableList.of(argsValue)));
            return result;
        }
    }

    /**
     * Allows the equivalent of the ternary operator in GSS, using three
     * {@code @def} statements as inputs. This GSS:
     * <p>
     * {@code @def MYDEF selectFrom(FOO, BAR, BAZ);}
     * <p>
     * implies:
     * <p>
     * {@code MYDEF = FOO ? BAR : BAZ;}
     * <p>
     * So this gss:
     * <p>
     * {@code @def FOO true;}
     * <p>
     * then implies:
     * <p>
     * {@code MYDEF = BAR;}
     */
    public static class SelectFrom implements GssFunction {
        @Override
        public Integer getNumExpectedArguments() {
            return 3;
        }

        @Override
        public List<CssValueNode> getCallResultNodes(List<CssValueNode> args, ErrorManager errorManager) {
            return ImmutableList.of("true".equals(args.get(0).getValue()) ? args.get(1) : args.get(2));
        }

        @Override
        public String getCallResultString(List<String> args) {
            return "true".equals(args.get(0)) ? args.get(1) : args.get(2);
        }
    }

    public static GssFunctionException error(CssValueNode node, String errorMessage, ErrorManager errorManager) {
        return error(errorMessage, errorManager, node.getSourceCodeLocation());
    }

    private static GssFunctionException error(String errorMessage, ErrorManager errorManager,
            SourceCodeLocation location) {
        if (errorManager != null) {
            errorManager.report(new GssError(errorMessage, location));
        }
        return new GssFunctionException(errorMessage);
    }

    private static CssNumericNode getSizeNode(CssValueNode valueNode, ErrorManager errorManager,
            boolean isUnitOptional) throws GssFunctionException {
        SourceCodeLocation location = valueNode.getSourceCodeLocation();
        if (valueNode instanceof CssNumericNode) {
            CssNumericNode node = (CssNumericNode) valueNode;
            checkSize(node.getNumericPart(), node.getUnit(), errorManager, location, isUnitOptional);
            return node;
        }
        String message = "Size must be a CssNumericNode with a unit or 0; " + "was: " + valueNode.toString();
        throw error(message, errorManager, location);
    }

    private static void checkSize(String valueString, String unit, ErrorManager errorManager,
            SourceCodeLocation location, boolean isUnitOptional) throws GssFunctionException {
        if (unit.equals(CssNumericNode.NO_UNITS)) {
            Double value = Double.parseDouble(valueString);
            if (value != 0.0 && !isUnitOptional) {
                String message = "Size must be 0 or have a unit; was: " + valueString + unit;
                throw error(message, errorManager, location);
            }
        }
    }

    /**
     * Helper method for implementors of GssFunction to allow the creation of
     * a url string in the GSS.
     *
     * @param url The url of the image to add.
     * @return The proper GSS url string.
     */
    public static String createUrl(String url) {
        return CssFunctionNode.Function.byName("url") + "(" + url + ")";
    }

    private static final class Size {
        final String size;
        final String units;

        public Size(String size, String units) {
            this.size = size;
            this.units = units;
        }
    }

    /**
     * Helper class for checking if a size string contains units. This class is equivalent to
     * {@link CharMatcher#JAVA_LETTER} except that it also accepts {@code %}.
     */
    private static final CharMatcher UNIT_MATCHER = new CharMatcher() {
        @Override
        public boolean matches(char c) {
            return Character.isLetter(c) || c == '%';
        }

        @Override
        public String toString() {
            return "GssFunctions.UNIT_MATCHER";
        }
    };

    private static Size parseSize(String sizeWithUnits, boolean isUnitOptional) throws GssFunctionException {
        int unitIndex = UNIT_MATCHER.indexIn(sizeWithUnits);
        String size = unitIndex > 0 ? sizeWithUnits.substring(0, unitIndex) : sizeWithUnits;
        String units = unitIndex > 0 ? sizeWithUnits.substring(unitIndex) : CssNumericNode.NO_UNITS;
        checkSize(size, units, null /* errorManager */, null /* location */, isUnitOptional);
        return new Size(size, units);
    }
}