package com.github.sommeri.less4j.core.compiler.expressions;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.imageio.ImageIO;
import com.github.sommeri.less4j.LessCompiler.Configuration;
import com.github.sommeri.less4j.LessSource;
import com.github.sommeri.less4j.LessSource.CannotReadFile;
import com.github.sommeri.less4j.LessSource.FileNotFound;
import com.github.sommeri.less4j.LessSource.StringSourceException;
import com.github.sommeri.less4j.core.ast.ASTCssNodeType;
import com.github.sommeri.less4j.core.ast.ColorExpression;
import com.github.sommeri.less4j.core.ast.CssString;
import com.github.sommeri.less4j.core.ast.Expression;
import com.github.sommeri.less4j.core.ast.FaultyExpression;
import com.github.sommeri.less4j.core.ast.FunctionExpression;
import com.github.sommeri.less4j.core.ast.IdentifierExpression;
import com.github.sommeri.less4j.core.ast.ListExpression;
import com.github.sommeri.less4j.core.ast.ListExpressionOperator;
import com.github.sommeri.less4j.core.ast.NumberExpression;
import com.github.sommeri.less4j.core.ast.NumberExpression.Dimension;
import com.github.sommeri.less4j.core.parser.ConversionUtils;
import com.github.sommeri.less4j.core.parser.HiddenTokenAwareTree;
import com.github.sommeri.less4j.core.problems.ProblemsHandler;
import com.github.sommeri.less4j.nodemime.NodeMime;
import com.github.sommeri.less4j.utils.InStringCssPrinter;
import com.github.sommeri.less4j.utils.MathUtils;
import com.github.sommeri.less4j.utils.PrintUtils;
public class MiscFunctions extends BuiltInFunctionsPack {
protected static final String COLOR = "color";
protected static final String UNIT = "unit";
protected static final String GET_UNIT = "get-unit";
protected static final String CONVERT = "convert";
protected static final String EXTRACT = "extract";
protected static final String DATA_URI = "data-uri";
protected static final String IMAGE_SIZE = "image-size";
protected static final String IMAGE_WIDTH = "image-width";
protected static final String IMAGE_HEIGHT = "image-height";
protected static final String SVG_GRADIENT = "svg-gradient";
private final Configuration configuration;
private Map<String, Function> allFunctions;
private static Map<String, Function> STATIC_FUNCTIONS = new HashMap<String, Function>();
static {
STATIC_FUNCTIONS.put(COLOR, new Color());
STATIC_FUNCTIONS.put(UNIT, new Unit());
STATIC_FUNCTIONS.put(GET_UNIT, new GetUnit());
STATIC_FUNCTIONS.put(CONVERT, new Convert());
STATIC_FUNCTIONS.put(EXTRACT, new Extract());
STATIC_FUNCTIONS.put(IMAGE_SIZE, new ImageSize());
STATIC_FUNCTIONS.put(IMAGE_WIDTH, new ImageWidth());
STATIC_FUNCTIONS.put(IMAGE_HEIGHT, new ImageHeight());
STATIC_FUNCTIONS.put(SVG_GRADIENT, new SvgGradient());
}
public MiscFunctions(ProblemsHandler problemsHandler, Configuration configuration) {
super(problemsHandler);
this.configuration = configuration;
}
@Override
protected Map<String, Function> getFunctions() {
if (allFunctions == null) {
allFunctions = new HashMap<String, Function>(STATIC_FUNCTIONS);
allFunctions.put(DATA_URI, new DataUri(configuration));
}
return allFunctions;
}
}
class Color extends CatchAllMultiParameterFunction {
@Override
protected Expression evaluate(List<Expression> splitParameters, ProblemsHandler problemsHandler, FunctionExpression functionCall, HiddenTokenAwareTree token) {
CssString string = (CssString) splitParameters.get(0);
String text = string.getValue();
// this does a bit more then less.js: it is able to parse named colors
ColorExpression parsedColor = ConversionUtils.parseColor(token, text);
if (parsedColor == null) {
FaultyExpression faultyExpression = new FaultyExpression(token);
problemsHandler.notAColor(faultyExpression, text);
}
return parsedColor;
}
@Override
protected int getMinParameters() {
return 1;
}
@Override
protected int getMaxParameters() {
return 1;
}
@Override
protected boolean validateParameter(Expression parameter, int position, ProblemsHandler problemsHandler) {
return validateParameterTypeReportError(parameter, problemsHandler, ASTCssNodeType.STRING_EXPRESSION);
}
@Override
protected String getName() {
return MiscFunctions.COLOR;
}
}
class Unit extends CatchAllMultiParameterFunction {
private TypesConversionUtils conversionUtils = new TypesConversionUtils();
@Override
protected Expression evaluate(List<Expression> splitParameters, ProblemsHandler problemsHandler, FunctionExpression functionCall, HiddenTokenAwareTree token) {
NumberExpression dimension = (NumberExpression) splitParameters.get(0);
String unit = splitParameters.size() > 1 ? conversionUtils.contentToString(splitParameters.get(1)) : null;
String newSuffix;
Dimension newDimension;
if (unit != null) {
newSuffix = unit;
newDimension = Dimension.forSuffix(newSuffix);
} else {
newSuffix = "";
newDimension = Dimension.NUMBER;
}
return new NumberExpression(token, dimension.getValueAsDouble(), newSuffix, null, newDimension);
}
@Override
protected int getMinParameters() {
return 1;
}
@Override
protected int getMaxParameters() {
return 2;
}
@Override
protected boolean validateParameter(Expression parameter, int position, ProblemsHandler problemsHandler) {
switch (position) {
case 0:
return validateParameterTypeReportError(parameter, problemsHandler, ASTCssNodeType.NUMBER);
case 1:
return validateParameterTypeReportError(parameter, problemsHandler, ASTCssNodeType.IDENTIFIER_EXPRESSION, ASTCssNodeType.STRING_EXPRESSION, ASTCssNodeType.ESCAPED_VALUE);
}
return false;
}
@Override
protected String getName() {
return MiscFunctions.UNIT;
}
}
class GetUnit extends CatchAllMultiParameterFunction {
@Override
protected Expression evaluate(List<Expression> splitParameters, ProblemsHandler problemsHandler, FunctionExpression functionCall, HiddenTokenAwareTree token) {
NumberExpression dimension = (NumberExpression) splitParameters.get(0);
return new IdentifierExpression(token, dimension.getSuffix()); // not sure
// about the
// type
}
@Override
protected int getMinParameters() {
return 1;
}
@Override
protected int getMaxParameters() {
return 1;
}
@Override
protected boolean validateParameter(Expression parameter, int position, ProblemsHandler problemsHandler) {
switch (position) {
case 0:
return validateParameterTypeReportError(parameter, problemsHandler, ASTCssNodeType.NUMBER);
}
return false;
}
@Override
protected String getName() {
return MiscFunctions.GET_UNIT;
}
}
class Convert extends CatchAllMultiParameterFunction {
private TypesConversionUtils conversionUtils = new TypesConversionUtils();
@Override
protected Expression evaluate(List<Expression> splitParameters, ProblemsHandler problemsHandler, FunctionExpression functionCall, HiddenTokenAwareTree token) {
NumberExpression value = (NumberExpression) splitParameters.get(0);
return value.convertIfPossible(conversionUtils.contentToString(splitParameters.get(1)));
}
@Override
protected int getMinParameters() {
return 2;
}
@Override
protected int getMaxParameters() {
return 2;
}
@Override
protected boolean validateParameter(Expression parameter, int position, ProblemsHandler problemsHandler) {
switch (position) {
case 0:
return validateParameterTypeReportError(parameter, problemsHandler, ASTCssNodeType.NUMBER);
case 1:
return validateParameterTypeReportError(parameter, problemsHandler, ASTCssNodeType.IDENTIFIER_EXPRESSION, ASTCssNodeType.STRING_EXPRESSION, ASTCssNodeType.ESCAPED_VALUE);
}
return false;
}
@Override
protected String getName() {
return MiscFunctions.CONVERT;
}
}
class Extract extends CatchAllMultiParameterFunction {
@Override
protected Expression evaluate(List<Expression> splitParameters, ProblemsHandler problemsHandler, FunctionExpression functionCall, HiddenTokenAwareTree token) {
Expression firstParameter = splitParameters.get(0);
NumberExpression index = (NumberExpression) splitParameters.get(1);
if (isList(firstParameter)) {
List<Expression> values = collect((ListExpression) firstParameter);
return values.get(index.getValueAsDouble().intValue() - 1);
}
if (MathUtils.equals(index.getValueAsDouble(), 1.0)) {
return firstParameter;
}
return functionCall;
}
private boolean isList(Expression value) {
return value.getType() == ASTCssNodeType.LIST_EXPRESSION;
}
private List<Expression> collect(ListExpression values) {
return values.getExpressions();
}
@Override
protected int getMinParameters() {
return 2;
}
@Override
protected int getMaxParameters() {
return 2;
}
@Override
protected boolean validateParameter(Expression parameter, int position, ProblemsHandler problemsHandler) {
switch (position) {
case 0:
return true;
case 1:
return validateParameterTypeReportError(parameter, problemsHandler, ASTCssNodeType.NUMBER);
}
return false;
}
@Override
protected String getName() {
return MiscFunctions.EXTRACT;
}
}
class DataUri extends CatchAllMultiParameterFunction {
private NodeMime mime = new NodeMime();
private static final int DATA_URI_MAX_KB = 32;
private boolean ieCompatibility;
public DataUri(Configuration configuration) {
this.ieCompatibility = configuration.hasIeCompatibility();
}
@Override
protected Expression evaluate(List<Expression> splitParameters, ProblemsHandler problemsHandler, FunctionExpression functionCall, HiddenTokenAwareTree token) {
String mimetype = null;
String filename = null;
if (splitParameters.size() == 1) {
CssString filenameArg = (CssString) splitParameters.get(0);
filename = filenameArg.getValue();
} else {
CssString mimetypeArg = (CssString) splitParameters.get(0);
mimetype = mimetypeArg.getValue();
CssString filenameArg = (CssString) splitParameters.get(1);
filename = filenameArg.getValue();
}
String[] filenameParts = filename.split("#", 2);
filename = filenameParts[0];
String fragments = filenameParts.length > 1 ? "#" + filenameParts[1] : "";
if (mimetype == null)
mimetype = guessMimetype(filename);
LessSource source = token.getSource();
try {
LessSource dataSource = source.relativeSource(filename);
byte[] data = dataSource.getBytes();
String encodedData = encodeDataUri(mimetype, data);
// **** less.js comment - flag is not implemented yet ****
// IE8 cannot handle a data-uri larger than 32KB. If this is exceeded
// and the --ieCompat flag is enabled, return a normal url() instead.
int encodedSizeInKB = encodedData.length() / 1024;
if (encodedSizeInKB >= DATA_URI_MAX_KB && ieCompatibility) {
problemsHandler.warnIE8UnsafeDataUri(functionCall, filename, encodedSizeInKB, DATA_URI_MAX_KB);
FunctionExpression result = new FunctionExpression(token, "url", functionCall.getParameter().clone());
result.configureParentToAllChilds();
return result;
}
return toDataUri(token, mimetype, encodedData, fragments);
} catch (FileNotFound ex) {
problemsHandler.errorFileNotFound(functionCall, filename);
return new FaultyExpression(functionCall.getUnderlyingStructure());
} catch (CannotReadFile e) {
problemsHandler.errorFileCanNotBeRead(functionCall, filename);
return new FaultyExpression(functionCall.getUnderlyingStructure());
} catch (StringSourceException ex) {
// imports are relative to current file and we do not know its location
problemsHandler.errorFileReferenceNoBaseDirectory(functionCall, filename);
return new FaultyExpression(functionCall.getUnderlyingStructure());
}
}
private String guessMimetype(String filename) {
String mimetype;
mimetype = mime.lookupMime(filename);
String charset = mime.lookupCharset(mimetype);
if (!textCharset(charset) && !isSvg(mimetype)) {
mimetype += ";base64";
}
return mimetype;
}
private boolean isSvg(String mimetype) {
return "image/svg+xml".equals(mimetype);
}
private String encodeDataUri(String mimetype, byte[] data) {
if (mimetype != null && mimetype.toLowerCase().endsWith("base64")) {
return PrintUtils.base64Encode(data);
} else {
return PrintUtils.toUtf8AsUri(new String(data));
}
}
private boolean textCharset(String charset) {
return "UTF-8".equals(charset) || "US-ASCII".equals(charset);
}
private Expression toDataUri(HiddenTokenAwareTree token, String mimetype, String data, String fragments) {
StringBuilder value = new StringBuilder("data:");
value.append(mimetype).append(",").append(data).append(fragments);
CssString parameter = new CssString(token, value.toString(), "\"");
return new FunctionExpression(token, "url", parameter);
}
@Override
protected int getMinParameters() {
return 1;
}
@Override
protected int getMaxParameters() {
return 2;
}
@Override
protected boolean validateParameter(Expression parameter, int position, ProblemsHandler problemsHandler) {
return validateParameterTypeReportError(parameter, problemsHandler, ASTCssNodeType.STRING_EXPRESSION);
}
@Override
protected String getName() {
return MiscFunctions.DATA_URI;
}
}
class ImageSize extends CatchAllMultiParameterFunction {
@Override
protected Expression evaluate(List<Expression> splitParameters, ProblemsHandler problemsHandler, FunctionExpression functionCall, HiddenTokenAwareTree token) {
CssString filenameArg = (CssString) splitParameters.get(0);
String filename = filenameArg.getValue();
LessSource source = token.getSource();
try {
LessSource dataSource = source.relativeSource(filename);
byte[] data = dataSource.getBytes();
BufferedImage image = ImageIO.read(new ByteArrayInputStream(data));
if (image == null) {
problemsHandler.errorUnknownImageFileFormat(functionCall, filename);
return new FaultyExpression(functionCall.getUnderlyingStructure());
}
int width = image.getWidth();
int height = image.getHeight();
return toSizeNumber(functionCall.getUnderlyingStructure(), width, height);
} catch (FileNotFound ex) {
problemsHandler.errorFileNotFound(functionCall, filename);
return new FaultyExpression(functionCall.getUnderlyingStructure());
} catch (CannotReadFile e) {
problemsHandler.errorFileCanNotBeRead(functionCall, filename);
return new FaultyExpression(functionCall.getUnderlyingStructure());
} catch (IOException e) {
problemsHandler.errorFileCanNotBeRead(functionCall, filename);
return new FaultyExpression(functionCall.getUnderlyingStructure());
} catch (StringSourceException ex) {
// imports are relative to current file and we do not know its location
problemsHandler.errorFileReferenceNoBaseDirectory(functionCall, filename);
return new FaultyExpression(functionCall.getUnderlyingStructure());
}
}
protected Expression toSizeNumber(HiddenTokenAwareTree token, int width, int height) {
Expression widthExp = toPixels(token, width);
Expression heightExp = toPixels(token, height);
return new ListExpression(token, Arrays.asList(widthExp, heightExp), new ListExpressionOperator(token, ListExpressionOperator.Operator.EMPTY_OPERATOR));
}
protected Expression toPixels(HiddenTokenAwareTree token, int width) {
return new NumberExpression(token, (double) width, "px", null, Dimension.LENGTH);
}
@Override
protected int getMinParameters() {
return 1;
}
@Override
protected int getMaxParameters() {
return 1;
}
@Override
protected boolean validateParameter(Expression parameter, int position, ProblemsHandler problemsHandler) {
return validateParameterTypeReportError(parameter, problemsHandler, ASTCssNodeType.STRING_EXPRESSION);
}
@Override
protected String getName() {
return MiscFunctions.IMAGE_SIZE;
}
}
class ImageWidth extends ImageSize {
protected Expression toSizeNumber(HiddenTokenAwareTree token, int width, int height) {
Expression widthExp = toPixels(token, width);
return widthExp;
}
@Override
protected String getName() {
return MiscFunctions.IMAGE_WIDTH;
}
}
class ImageHeight extends ImageSize {
protected Expression toSizeNumber(HiddenTokenAwareTree token, int width, int height) {
Expression heightExp = toPixels(token, height);
return heightExp;
}
@Override
protected String getName() {
return MiscFunctions.IMAGE_HEIGHT;
}
}
class SvgGradient extends CatchAllMultiParameterFunction {
private final TypesConversionUtils conversions = new TypesConversionUtils();
@Override
protected Expression evaluate(List<Expression> splitParameters, ProblemsHandler problemsHandler, FunctionExpression functionCall, HiddenTokenAwareTree token) {
String direction = toDirection(splitParameters.get(0)), gradientDirectionSvg = "";
List<Expression> stops = extractStops(splitParameters);
if (stops == null || stops.size() < 2) {
problemsHandler.errorSvgGradientArgument(functionCall);
return new FaultyExpression(functionCall.getUnderlyingStructure());
}
String gradientType = "linear", rectangleDimension = "x=\"0\" y=\"0\" width=\"1\" height=\"1\"";
boolean useBase64 = true;
if ("to bottom".equals(direction)) {
gradientDirectionSvg = "x1=\"0%\" y1=\"0%\" x2=\"0%\" y2=\"100%\"";
} else if ("to right".equals(direction)) {
gradientDirectionSvg = "x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"0%\"";
} else if ("to bottom right".equals(direction)) {
gradientDirectionSvg = "x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\"";
} else if ("to top right".equals(direction)) {
gradientDirectionSvg = "x1=\"0%\" y1=\"100%\" x2=\"100%\" y2=\"0%\"";
} else if (direction != null && direction.startsWith("ellipse")) {
gradientType = "radial";
gradientDirectionSvg = "cx=\"50%\" cy=\"50%\" r=\"75%\"";
rectangleDimension = "x=\"-50\" y=\"-50\" width=\"101\" height=\"101\"";
} else {
problemsHandler.wrongEnumeratedArgument(functionCall, "direction", "to bottom", "to right", "to bottom right", "to top right", "ellipse", "ellipse at center");
return new FaultyExpression(functionCall);
}
StringBuilder returner = new StringBuilder("<?xml version=\"1.0\" ?>");
returner.append("<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"100%\" height=\"100%\" viewBox=\"0 0 1 1\" preserveAspectRatio=\"none\">");
returner.append("<");
returner.append(gradientType);
returner.append("Gradient id=\"gradient\" gradientUnits=\"userSpaceOnUse\" ");
returner.append(gradientDirectionSvg);
returner.append(">");
Iterator<Expression> iterator = stops.iterator();
boolean isFirstStop = true;
while (iterator.hasNext()) {
Expression stop = iterator.next();
if (!addColorStop(returner, stop, isFirstStop, !iterator.hasNext(), functionCall, problemsHandler)) {
problemsHandler.errorSvgGradientArgument(functionCall);
return new FaultyExpression(functionCall);
}
isFirstStop = false;
}
returner.append("</").append(gradientType).append("Gradient>");
returner.append("<rect ").append(rectangleDimension).append(" fill=\"url(#gradient)\" /></svg>");
String result = useBase64 ? PrintUtils.base64Encode(returner.toString().getBytes()) : returner.toString();
return toDataUri(functionCall.getUnderlyingStructure(), result, useBase64);
}
private List<Expression> extractStops(List<Expression> splitParameters) {
if (splitParameters.size() == 2) {
Expression expression = splitParameters.get(1);
if (ASTCssNodeType.LIST_EXPRESSION == expression.getType()) {
ListExpression list = (ListExpression) expression;
return list.getExpressions();
} else {
return null;
}
}
return splitParameters.subList(1, splitParameters.size());
}
private Expression toDataUri(HiddenTokenAwareTree token, String data, boolean useBase64) {
StringBuilder value = new StringBuilder("data:image/svg+xml");
if (useBase64)
value.append(";base64");
value.append(",").append(data);
CssString parameter = new CssString(token, value.toString(), "\'");
return new FunctionExpression(token, "url", parameter);
}
private boolean addColorStop(StringBuilder returner, Expression colorStop, boolean isFirst, boolean isLast, FunctionExpression errorNode, ProblemsHandler problemsHandler) {
if (colorStop.getType() == ASTCssNodeType.LIST_EXPRESSION) {
ListExpression list = (ListExpression) colorStop;
List<Expression> expressions = list.getExpressions();
if (expressions.isEmpty() || expressions.size() > 2) {
return false;
}
Expression color = expressions.get(0);
Expression position = expressions.size() > 1 ? expressions.get(1) : null;
if (!addColorStop(returner, color, position, isFirst, isLast, errorNode, problemsHandler))
return false;
} else {
if (!addColorStop(returner, colorStop, null, isFirst, isLast, errorNode, problemsHandler))
return false;
}
return true;
}
private boolean addColorStop(StringBuilder returner, Expression colorE, Expression position, boolean isFirst, boolean isLast, FunctionExpression errorNode, ProblemsHandler problemsHandler) {
if (colorE.getType() != ASTCssNodeType.COLOR_EXPRESSION) {
problemsHandler.errorSvgGradientArgument(errorNode);
return false;
}
if (!isLast && !isFirst && position == null) {
problemsHandler.errorSvgGradientArgument(errorNode);
return false;
}
ColorExpression color = (ColorExpression) colorE;
String positionValue = position != null ? toCss(position) : isFirst ? "0%" : "100%";
returner.append("<stop offset=\"").append(positionValue);
returner.append("\" stop-color=\"").append(color.getValueInHexadecimal());
returner.append("\"");
if (color.hasAlpha()) {
returner.append(" stop-opacity=\"").append(PrintUtils.formatNumber(color.getAlpha())).append("\"");
}
returner.append("/>");
return true;
}
private String toDirection(Expression direction) {
String result = conversions.contentToString(direction);
if (result != null)
return result;
return toCss(direction);
}
private String toCss(Expression direction) {
InStringCssPrinter printer = new InStringCssPrinter();
printer.append(direction);
return printer.toString();
}
@Override
protected boolean validateParameter(Expression parameter, int position, ProblemsHandler problemsHandler) {
return true;
}
@Override
protected int getMinParameters() {
return 2;
}
@Override
protected int getMaxParameters() {
return Integer.MAX_VALUE;
}
@Override
protected String getName() {
return MiscFunctions.SVG_GRADIENT;
}
}