/******************************************************************************* * Copyright 2012-present Pixate, 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.pixate.freestyle.styling.parsing; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import android.annotation.SuppressLint; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.net.Uri; import android.util.DisplayMetrics; import android.widget.GridView; import com.pixate.freestyle.cg.math.PXDimension; import com.pixate.freestyle.cg.math.PXOffsets; import com.pixate.freestyle.cg.paints.PXImagePaint; import com.pixate.freestyle.cg.paints.PXLinearGradient; import com.pixate.freestyle.cg.paints.PXLinearGradient.PXLinearGradientDirection; import com.pixate.freestyle.cg.paints.PXPaint; import com.pixate.freestyle.cg.paints.PXPaintGroup; import com.pixate.freestyle.cg.paints.PXRadialGradient; import com.pixate.freestyle.cg.paints.PXSolidPaint; import com.pixate.freestyle.cg.shadow.PXShadow; import com.pixate.freestyle.cg.shadow.PXShadowGroup; import com.pixate.freestyle.cg.shadow.PXShadowPaint; import com.pixate.freestyle.styling.infos.PXAnimationInfo; import com.pixate.freestyle.styling.infos.PXAnimationInfo.PXAnimationDirection; import com.pixate.freestyle.styling.infos.PXAnimationInfo.PXAnimationFillMode; import com.pixate.freestyle.styling.infos.PXAnimationInfo.PXAnimationPlayState; import com.pixate.freestyle.styling.infos.PXAnimationInfo.PXAnimationTimingFunction; import com.pixate.freestyle.styling.infos.PXBorderInfo; import com.pixate.freestyle.styling.infos.PXBorderInfo.PXBorderStyle; import com.pixate.freestyle.styling.stylers.PXStylerContext.GridStyle.PXColumnStretchMode; import com.pixate.freestyle.util.CollectionUtil; import com.pixate.freestyle.util.PXLog; import com.pixate.freestyle.util.SVGColors; import com.pixate.freestyle.util.Size; import com.pixate.freestyle.util.StringUtil; @SuppressLint("DefaultLocale") public class PXValueParser { private List<PXStylesheetLexeme> lexemes = null; private PXStylesheetLexeme currentLexeme; private int lexemeIndex = 0; private List<String> errors; private String filename; /* STATIC */ // TODO Probably will change value's data type. PorterDuff (I guess) doesn't // have everything. private static final Map<String, PorterDuff.Mode> BLEND_MODE_MAP; private static final Set<PXStylesheetTokenType> NUMBER_SET; private static final Set<PXStylesheetTokenType> COLOR_SET; private static final Set<PXStylesheetTokenType> PAINT_SET; private static final String TAG = PXValueParser.class.getSimpleName(); // Lazy inits private static Set<String> ANIMATION_KEYWORDS; private static Map<String, PXBorderStyle> BORDER_STYLE_MAP; static { NUMBER_SET = EnumSet.of(PXStylesheetTokenType.NUMBER, PXStylesheetTokenType.LENGTH); COLOR_SET = EnumSet.of(PXStylesheetTokenType.RGB, PXStylesheetTokenType.RGBA, PXStylesheetTokenType.HSB, PXStylesheetTokenType.HSBA, PXStylesheetTokenType.HSL, PXStylesheetTokenType.HSLA, PXStylesheetTokenType.HEX_COLOR, PXStylesheetTokenType.IDENTIFIER, PXStylesheetTokenType.ID); PAINT_SET = EnumSet.of(PXStylesheetTokenType.LINEAR_GRADIENT, PXStylesheetTokenType.RADIAL_GRADIENT, PXStylesheetTokenType.URL); PAINT_SET.addAll(COLOR_SET); BLEND_MODE_MAP = new HashMap<String, PorterDuff.Mode>(); BLEND_MODE_MAP.put("normal", PorterDuff.Mode.SRC_OVER); BLEND_MODE_MAP.put("multiply", PorterDuff.Mode.MULTIPLY); BLEND_MODE_MAP.put("screen", PorterDuff.Mode.SCREEN); // BLEND_MODE_MAP.put("overlay", PorterDuff.Mode.OVERLAY); OVERLAY // requires api 11 BLEND_MODE_MAP.put("darken", PorterDuff.Mode.DARKEN); BLEND_MODE_MAP.put("lighten", PorterDuff.Mode.LIGHTEN); // TODO BLEND_MODE_MAP.put("color-dodge", PorterDuff.Mode.); // TODO BLEND_MODE_MAP.put("color-burn", PorterDuff.Mode.); // TODO BLEND_MODE_MAP.put("soft-light", PorterDuff.Mode.); // TODO BLEND_MODE_MAP.put("hard-light", PorterDuff.Mode.); // TODO BLEND_MODE_MAP.put("difference", PorterDuff.Mode.); // TODO BLEND_MODE_MAP.put("exclusion", PorterDuff.Mode.); // TODO BLEND_MODE_MAP.put("hue", PorterDuff.Mode.); // TODO BLEND_MODE_MAP.put("saturation", PorterDuff.Mode.); // TODO BLEND_MODE_MAP.put("color", PorterDuff.Mode.); // TODO BLEND_MODE_MAP.put("luminosity", PorterDuff.Mode.); BLEND_MODE_MAP.put("clear", PorterDuff.Mode.CLEAR); // TODO BLEND_MODE_MAP.put("copy", PorterDuff.Mode.); BLEND_MODE_MAP.put("source-in", PorterDuff.Mode.SRC_IN); BLEND_MODE_MAP.put("source-out", PorterDuff.Mode.SRC_OUT); BLEND_MODE_MAP.put("source-atop", PorterDuff.Mode.SRC_ATOP); BLEND_MODE_MAP.put("destination-over", PorterDuff.Mode.DST_OVER); BLEND_MODE_MAP.put("destination-in", PorterDuff.Mode.DST_IN); BLEND_MODE_MAP.put("destination-out", PorterDuff.Mode.DST_OUT); BLEND_MODE_MAP.put("destination-atop", PorterDuff.Mode.DST_ATOP); BLEND_MODE_MAP.put("xor", PorterDuff.Mode.XOR); // TODO BLEND_MODE_MAP.put("plus-darker", PorterDuff.Mode.); // TODO BLEND_MODE_MAP.put("plus-lighter", PorterDuff.Mode.); } public static List<PXStylesheetLexeme> lexemesForSource(String source) { List<PXStylesheetLexeme> lexemes = new ArrayList<PXStylesheetLexeme>(); PXStylesheetLexer lexer = new PXStylesheetLexer(); lexer.setSource(source); PXStylesheetLexeme lexeme = lexer.nextLexeme(); while (!(lexeme == null || lexeme.getType() == PXStylesheetTokenType.EOF)) { lexemes.add(lexeme); lexeme = lexer.nextLexeme(); } if (PXLog.isLogging()) { PXLog.d(TAG, "Lexemes for source \"%s\"", source); for (PXStylesheetLexeme l : lexemes) { PXLog.d(TAG, "%s: %s", l.getTypeName().toUpperCase(Locale.US), l.getValue() .toString()); } } return lexemes; } /* PUBLIC METHODS */ public List<PXAnimationInfo> parseAnimationInfos(List<PXStylesheetLexeme> lexemes) { setupWithLexemes(lexemes); List<PXAnimationInfo> items = new ArrayList<PXAnimationInfo>(); try { items.add(parseAnimationInfo()); while (isType(PXStylesheetTokenType.COMMA)) { advance(); items.add(parseAnimationInfo()); } } catch (Exception e) { exceptionWithMessage(e.getMessage()); } return items; } public List<PXAnimationInfo> parseTransitionInfos(List<PXStylesheetLexeme> lexemes) { setupWithLexemes(lexemes); List<PXAnimationInfo> items = new ArrayList<PXAnimationInfo>(); try { items.add(parseTransitionInfo()); while (isType(PXStylesheetTokenType.COMMA)) { advance(); items.add(parseTransitionInfo()); } } catch (Exception e) { exceptionWithMessage(e.getMessage()); } return items; } public Uri parseURL(List<PXStylesheetLexeme> lexemes) { setupWithLexemes(lexemes); return parseURL(); } public List<PXAnimationDirection> parseAnimationDirectionList(List<PXStylesheetLexeme> lexemes) { setupWithLexemes(lexemes); List<PXAnimationDirection> items = new ArrayList<PXAnimationDirection>(); try { if (isType(PXStylesheetTokenType.IDENTIFIER)) { items.add(this.getAnimationDirection()); advance(); while (isType(PXStylesheetTokenType.COMMA)) { // advance over ',' advance(); if (isType(PXStylesheetTokenType.IDENTIFIER)) { items.add(this.getAnimationDirection()); advance(); } else { exceptionWithMessage("Expected an animation direction after a comma in the times list"); } } } } catch (Exception e) { addError(e.getMessage()); } return items; } public List<PXAnimationFillMode> parseAnimationFillModeList(List<PXStylesheetLexeme> lexemes) { setupWithLexemes(lexemes); List<PXAnimationFillMode> items = new ArrayList<PXAnimationFillMode>(); try { if (isType(PXStylesheetTokenType.IDENTIFIER)) { items.add(this.getAnimationFillMode()); advance(); while (isType(PXStylesheetTokenType.COMMA)) { // advance over ',' advance(); if (isType(PXStylesheetTokenType.IDENTIFIER)) { items.add(this.getAnimationFillMode()); advance(); } else { exceptionWithMessage("Expected an animation fill mode after a comma in the times list"); } } } } catch (Exception e) { addError(e.getMessage()); } return items; } public List<PXAnimationPlayState> parseAnimationPlayStateList(List<PXStylesheetLexeme> lexemes) { setupWithLexemes(lexemes); List<PXAnimationPlayState> items = new ArrayList<PXAnimationPlayState>(); try { if (isType(PXStylesheetTokenType.IDENTIFIER)) { items.add(this.getAnimationPlayState()); advance(); while (isType(PXStylesheetTokenType.COMMA)) { // advance over ',' advance(); if (isType(PXStylesheetTokenType.IDENTIFIER)) { items.add(this.getAnimationPlayState()); advance(); } else { exceptionWithMessage("Expected an animation play state after a comma in the times list"); } } } } catch (Exception e) { addError(e.getMessage()); } return items; } public List<PXAnimationTimingFunction> parseAnimationTimingFunctionList( List<PXStylesheetLexeme> lexemes) { setupWithLexemes(lexemes); List<PXAnimationTimingFunction> items = new ArrayList<PXAnimationTimingFunction>(); try { if (isType(PXStylesheetTokenType.IDENTIFIER)) { items.add(this.getAnimationTimingFunction()); advance(); while (isType(PXStylesheetTokenType.COMMA)) { // advance over ',' advance(); if (isType(PXStylesheetTokenType.IDENTIFIER)) { items.add(this.getAnimationTimingFunction()); advance(); } else { exceptionWithMessage("Expected an animation timing function " + "after a comma in the times list"); } } } } catch (Exception e) { addError(e.getMessage()); } return items; } public PXBorderInfo parseBorder(List<PXStylesheetLexeme> lexemes, DisplayMetrics displayMetrics) { setupWithLexemes(lexemes); PXBorderInfo settings = new PXBorderInfo(); try { if (isInTypeSet(NUMBER_SET)) { settings.setWidth(readNumber(displayMetrics)); } if (isType(PXStylesheetTokenType.IDENTIFIER) && !isSVGColorName()) { settings.setStyle(parseBorderStyle()); } if (isInTypeSet(PAINT_SET)) { settings.setPaint(parseSinglePaint()); } } catch (Exception e) { addError(e.getMessage()); } return settings; } public List<Size> parseBorderRadiusList(List<PXStylesheetLexeme> lexemes, DisplayMetrics displayMetrics) { setupWithLexemes(lexemes); // @formatter:off float topLeftWidth, topLeftHeight, topRightWidth, topRightHeight, bottomRightWidth, bottomRightHeight, bottomLeftWidth, bottomLeftHeight; topLeftWidth = topLeftHeight = topRightWidth = topRightHeight = bottomRightWidth = bottomRightHeight = bottomLeftWidth = bottomLeftHeight = 0.0f; // @formatter:on try { PXOffsets xRadii = parseOffsets(displayMetrics); topLeftWidth = topLeftHeight = xRadii.getTop(); topRightWidth = topRightHeight = xRadii.getRight(); bottomRightWidth = bottomRightHeight = xRadii.getBottom(); bottomLeftWidth = bottomLeftHeight = xRadii.getLeft(); if (isType(PXStylesheetTokenType.SLASH)) { advance(); // past '/' PXOffsets yRadii = parseOffsets(displayMetrics); topLeftHeight = yRadii.getTop(); topRightHeight = yRadii.getRight(); bottomRightHeight = yRadii.getBottom(); bottomLeftHeight = yRadii.getLeft(); } } catch (Exception e) { addError(e.getMessage()); } return Arrays.asList(new Size(topLeftWidth, topLeftHeight), new Size(topRightWidth, topRightHeight), new Size(bottomRightWidth, bottomRightHeight), new Size( bottomLeftWidth, bottomLeftHeight)); } public PXBorderStyle parseBorderStyle(List<PXStylesheetLexeme> lexemes) { setupWithLexemes(lexemes); return parseBorderStyle(); } public List<PXBorderStyle> parseBorderStyleList(List<PXStylesheetLexeme> lexemes) { setupWithLexemes(lexemes); PXBorderStyle top, right, bottom, left; top = right = bottom = left = PXBorderStyle.NONE; try { if (isType(PXStylesheetTokenType.IDENTIFIER)) { top = right = bottom = left = parseBorderStyle(); } if (isType(PXStylesheetTokenType.IDENTIFIER)) { right = left = parseBorderStyle(); } if (isType(PXStylesheetTokenType.IDENTIFIER)) { bottom = parseBorderStyle(); } if (isType(PXStylesheetTokenType.IDENTIFIER)) { left = parseBorderStyle(); } } catch (Exception e) { addError(e.getMessage()); } return Arrays.asList(top, right, bottom, left); } public Integer parseColor(List<PXStylesheetLexeme> lexemes) { setupWithLexemes(lexemes); Integer result = null; try { result = getColor(); } catch (Exception e) { addError(e.getMessage()); } return result; } public float parseColumnWidth(List<PXStylesheetLexeme> lexemes, DisplayMetrics displayMetrics) { setupWithLexemes(lexemes); float result = 0.0f; try { result = getColumnWidth(displayMetrics); } catch (Exception e) { addError(e.getMessage()); } return result; } public int parseColumnGap(List<PXStylesheetLexeme> lexemes, DisplayMetrics displayMetrics) { setupWithLexemes(lexemes); int result = 0; // android uses 0 internally to indicate no setting for // spacing. try { result = getColumnGap(displayMetrics); } catch (Exception e) { addError(e.getMessage()); } return result; } public PXColumnStretchMode parseColumnStretchMode(List<PXStylesheetLexeme> lexemes) { setupWithLexemes(lexemes); PXColumnStretchMode result = PXColumnStretchMode.COLUMN_WIDTH; // Android's // default try { result = getColumnStretchMode(); } catch (Exception e) { addError(e.getMessage()); } return result; } public int parseRowGap(List<PXStylesheetLexeme> lexemes, DisplayMetrics displayMetrics) { setupWithLexemes(lexemes); int result = 0; // android uses 0 internally to indicate no setting for // spacing. try { result = getRowGap(displayMetrics); } catch (Exception e) { addError(e.getMessage()); } return result; } public float parseFloat(List<PXStylesheetLexeme> lexemes, DisplayMetrics displayMetrics) { setupWithLexemes(lexemes); float result = 0.0f; try { result = getFloatValue(displayMetrics); } catch (Exception e) { addError(e.getMessage()); } return result; } public List<Float> parseFloatList(List<PXStylesheetLexeme> lexemes) { setupWithLexemes(lexemes); List<Float> items = new ArrayList<Float>(); try { if (isType(PXStylesheetTokenType.NUMBER)) { items.add(Float.parseFloat((String) currentLexeme.getValue())); advance(); while (isType(PXStylesheetTokenType.COMMA)) { if (isType(PXStylesheetTokenType.NUMBER)) { items.add(Float.parseFloat((String) currentLexeme.getValue())); advance(); } else { exceptionWithMessage("Expected an number after a comma in the number list"); } } } } catch (Exception e) { addError(e.getMessage()); } return items; } public int parseColumnCount(List<PXStylesheetLexeme> lexemes) { setupWithLexemes(lexemes); int result = GridView.AUTO_FIT; try { result = getColumnCount(); } catch (Exception e) { addError(e.getMessage()); } return result; } public PXOffsets parseInsets(List<PXStylesheetLexeme> lexemes, DisplayMetrics displayMetrics) { return parseOffsets(lexemes, displayMetrics); } public List<String> parseNameList(List<PXStylesheetLexeme> lexemes) { setupWithLexemes(lexemes); List<String> items = new ArrayList<String>(); try { if (isType(PXStylesheetTokenType.IDENTIFIER)) { items.add((String) currentLexeme.getValue()); advance(); while (isType(PXStylesheetTokenType.COMMA)) { // Advance over "," advance(); if (isType(PXStylesheetTokenType.IDENTIFIER)) { items.add((String) currentLexeme.getValue()); advance(); } else { exceptionWithMessage("Expected an identifier after a comma in the name list"); } } } } catch (Exception e) { addError(e.getMessage()); } return items; } public PXOffsets parseOffsets(List<PXStylesheetLexeme> lexemes, DisplayMetrics displayMetrics) { setupWithLexemes(lexemes); return parseOffsets(displayMetrics); } public PXOffsets parseOffsets(DisplayMetrics displayMetrics) { float top, right, bottom, left; top = right = bottom = left = 0.0f; try { // one number if (isInTypeSet(NUMBER_SET)) { top = right = bottom = left = readNumber(displayMetrics); } // two numbers if (isInTypeSet(NUMBER_SET)) { right = left = readNumber(displayMetrics); } // three numbers if (isInTypeSet(NUMBER_SET)) { bottom = readNumber(displayMetrics); } // four numbers if (isInTypeSet(NUMBER_SET)) { left = readNumber(displayMetrics); } } catch (Exception e) { addError(e.getMessage()); } return new PXOffsets(top, right, bottom, left); } public PXPaint parsePaint(List<PXStylesheetLexeme> lexemes) { setupWithLexemes(lexemes); PXPaint result = null; try { result = parseSinglePaint(); if (isType(PXStylesheetTokenType.COMMA)) { PXPaintGroup group = new PXPaintGroup(); group.addPaint(result); while (isType(PXStylesheetTokenType.COMMA)) { // Advance over comma advance(); group.addPaint(parseSinglePaint()); } result = group; } } catch (Exception e) { addError(e.getMessage()); } return result; } public List<PXPaint> parsePaints(List<PXStylesheetLexeme> lexemes) { setupWithLexemes(lexemes); PXPaint topPaint, bottomPaint, rightPaint, leftPaint; topPaint = bottomPaint = rightPaint = leftPaint = null; try { // NOTE: There can be zero or more paints in the token stream. If // the very first item is not a paint, we go ahead and default all // colors to black. The following if-blocks will fail, so doing it // here is similar to setting a default value for these items. if (isInTypeSet(PAINT_SET)) { topPaint = rightPaint = bottomPaint = leftPaint = parseSinglePaint(); } else { topPaint = rightPaint = bottomPaint = leftPaint = PXSolidPaint .createPaintWithColor(Color.BLACK); } if (isInTypeSet(PAINT_SET)) { rightPaint = leftPaint = parseSinglePaint(); } if (isInTypeSet(PAINT_SET)) { bottomPaint = parseSinglePaint(); } if (isInTypeSet(PAINT_SET)) { leftPaint = parseSinglePaint(); } } catch (Exception e) { addError(e.getMessage()); } return Arrays.asList(topPaint, rightPaint, bottomPaint, leftPaint); } public float parseSeconds(List<PXStylesheetLexeme> lexemes) { setupWithLexemes(lexemes); float seconds = 0.0f; try { seconds = getSecondsValue(); } catch (Exception e) { addError(e.getMessage()); } return seconds; } public List<Float> parseSecondsList(List<PXStylesheetLexeme> lexemes) { setupWithLexemes(lexemes); List<Float> items = new ArrayList<Float>(); try { if (isType(PXStylesheetTokenType.TIME)) { items.add(getSecondsValue()); advance(); while (isType(PXStylesheetTokenType.COMMA)) { // advance over "," advance(); if (isType(PXStylesheetTokenType.TIME)) { items.add(getSecondsValue()); advance(); } else { exceptionWithMessage("Expected an time after a comma in the times list"); } } } } catch (Exception e) { addError(e.getMessage()); } return items; } public PXShadowPaint parseShadow(List<PXStylesheetLexeme> lexemes, DisplayMetrics displayMetrics) { setupWithLexemes(lexemes); PXShadowPaint result = null; try { result = parseShadow(displayMetrics); if (isType(PXStylesheetTokenType.COMMA)) { PXShadowGroup group = new PXShadowGroup(); group.add(result); while (isType(PXStylesheetTokenType.COMMA)) { // skip over "," advance(); group.add(parseShadow(displayMetrics)); } result = group; } } catch (Exception e) { addError(e.getMessage()); } return result; } public Size parseSize(List<PXStylesheetLexeme> lexemes, DisplayMetrics displayMetrics) { setupWithLexemes(lexemes); float width = 0.0f; float height = 0.0f; try { // one number if (isInTypeSet(NUMBER_SET)) { width = height = readNumber(displayMetrics); } // two numbers if (isInTypeSet(NUMBER_SET)) { height = readNumber(displayMetrics); } } catch (Exception e) { addError(e.getMessage()); } return new Size(width, height); } public void setFilename(String name) { filename = name; } public String getFilename() { return filename; } /* (end public) */ /* PRIVATE METHODS */ private void addError(String message) { String offset = isType(PXStylesheetTokenType.EOF) ? "EOF" : (currentLexeme != null ? String .valueOf(currentLexeme.getOffset()) : "Null lexeme"); addError(message, filename, offset); } private void addError(String message, String filename, String offset) { String amendedError; if (!StringUtil.isEmpty(filename)) { amendedError = MessageFormat.format("[PXEngine.ParseError, file={0}, offset={1}]: {2}", filename, offset, message); } else { amendedError = MessageFormat.format("[PXEngine.ParseError, offset={0}]: {1}", offset, message); } internalAddError(amendedError); // TODO equiv of Obj-C [PXEngine.configuration // sendParseMessage:ammendedError]; } private void internalAddError(String message) { if (errors == null) { errors = new ArrayList<String>(); } errors.add(message); } private void setupWithLexemes(List<PXStylesheetLexeme> lexemes) { clearErrors(); setLexemes(lexemes); advance(); } private void clearErrors() { if (errors != null) { errors.clear(); } } private PXStylesheetLexeme advance() { if (CollectionUtil.isEmpty(lexemes)) { return null; } return currentLexeme = lexemeIndex < lexemes.size() ? lexemes.get(lexemeIndex++) : null; } private void advanceIfIsType(PXStylesheetTokenType tokenType) { if (isType(tokenType)) { advance(); } } private PXStylesheetLexeme assertTypeAndAdvance(PXStylesheetTokenType tokenType) { assertType(tokenType); return advance(); } private void assertTypeInSet(Set<PXStylesheetTokenType> typeSet) { if (!isInTypeSet(typeSet)) { StringBuilder sb = new StringBuilder(); for (PXStylesheetTokenType tokenType : typeSet) { if (sb.length() > 0) { sb.append(", "); } sb.append(tokenType.name()); } exceptionWithMessage(MessageFormat.format( "Expected a token of one of these types: {0}", sb.toString())); } } private void setLexemes(List<PXStylesheetLexeme> lexemes) { this.lexemes = lexemes; this.lexemeIndex = 0; } private PXBorderStyle parseBorderStyle() { synchronized (PXValueParser.class) { if (BORDER_STYLE_MAP == null) { // One-time init BORDER_STYLE_MAP = new HashMap<String, PXBorderStyle>(10); BORDER_STYLE_MAP.put("none", PXBorderStyle.NONE); BORDER_STYLE_MAP.put("hidden", PXBorderStyle.HIDDEN); BORDER_STYLE_MAP.put("dotted", PXBorderStyle.DOTTED); BORDER_STYLE_MAP.put("dashed", PXBorderStyle.DASHED); BORDER_STYLE_MAP.put("solid", PXBorderStyle.SOLID); BORDER_STYLE_MAP.put("double", PXBorderStyle.DOUBLE); BORDER_STYLE_MAP.put("groove", PXBorderStyle.GROOVE); BORDER_STYLE_MAP.put("ridge", PXBorderStyle.RIDGE); BORDER_STYLE_MAP.put("inset", PXBorderStyle.INSET); BORDER_STYLE_MAP.put("outset", PXBorderStyle.OUTSET); } } PXBorderStyle result = PXBorderStyle.NONE; try { if (isType(PXStylesheetTokenType.IDENTIFIER)) { String text = (String) currentLexeme.getValue(); PXBorderStyle value = BORDER_STYLE_MAP.get(text.toLowerCase()); if (value != null) { result = value; } advance(); } } catch (Exception e) { addError(e.getMessage()); } return result; } private PXShadowPaint parseShadow(DisplayMetrics displayMetrics) { PXShadow result = new PXShadow(); if (isIdentifierWithName("inset")) { result.setIsInset(true); advance(); } // grab required x-offset assertTypeInSet(NUMBER_SET); result.setHorizontalOffset(getFloatValue(displayMetrics)); // grab required y-offset assertTypeInSet(NUMBER_SET); result.setVerticalOffset(getFloatValue(displayMetrics)); // Next two lengths are optional if (isType(PXStylesheetTokenType.LENGTH) || isType(PXStylesheetTokenType.NUMBER)) { result.setBlurDistance(getFloatValue(displayMetrics)); if (isType(PXStylesheetTokenType.LENGTH) || isType(PXStylesheetTokenType.NUMBER)) { result.setSpreadDistance(getFloatValue(displayMetrics)); } } // Color is optional result.setColor(isInTypeSet(COLOR_SET) ? getColor() : Color.BLACK); return result; } private boolean isType(PXStylesheetTokenType tokenType) { return currentLexeme != null && currentLexeme.getType() == tokenType; } private boolean isIdentifierWithName(String name) { return isType(PXStylesheetTokenType.IDENTIFIER) && name.equals((String) currentLexeme.getValue()); } private boolean isInTypeSet(Set<PXStylesheetTokenType> typeSet) { return currentLexeme != null && typeSet.contains(currentLexeme.getType()); } private boolean isSVGColorName() { if (!isType(PXStylesheetTokenType.IDENTIFIER)) { return false; } String value = (String) currentLexeme.getValue(); return SVGColors.has(value); } private PXAnimationInfo parseAnimationInfo() { synchronized (PXValueParser.class) { if (ANIMATION_KEYWORDS == null) { // One-time init ANIMATION_KEYWORDS = new HashSet<String>(); ANIMATION_KEYWORDS.addAll(PXAnimationDirection.getCssValueSet()); ANIMATION_KEYWORDS.addAll(PXAnimationFillMode.getCssValueSet()); ANIMATION_KEYWORDS.addAll(PXAnimationPlayState.getCssValueSet()); ANIMATION_KEYWORDS.addAll(PXAnimationTimingFunction.getCssValueSet()); } } PXAnimationInfo info = new PXAnimationInfo(); if (isType(PXStylesheetTokenType.IDENTIFIER) && !ANIMATION_KEYWORDS.contains(currentLexeme.getValue())) { info.animationName = (String) currentLexeme.getValue(); advance(); } if (isType(PXStylesheetTokenType.TIME)) { info.animationDuration = this.getSecondsValue(); } if (isType(PXStylesheetTokenType.IDENTIFIER) && PXAnimationTimingFunction.ofCssValue((String) currentLexeme.getValue()) != null) { info.animationTimingFunction = this.getAnimationTimingFunction(); } if (isType(PXStylesheetTokenType.TIME)) { info.animationDelay = this.getSecondsValue(); } if (isType(PXStylesheetTokenType.NUMBER)) { info.animationIterationCount = Integer.parseInt((String) currentLexeme.getValue()); advance(); } if (isType(PXStylesheetTokenType.IDENTIFIER) && PXAnimationDirection.ofCssValue((String) currentLexeme.getValue()) != null) { info.animationDirection = this.getAnimationDirection(); } if (isType(PXStylesheetTokenType.IDENTIFIER) && PXAnimationFillMode.ofCssValue((String) currentLexeme.getValue()) != null) { info.animationFillMode = this.getAnimationFillMode(); } if (isType(PXStylesheetTokenType.IDENTIFIER) && PXAnimationPlayState.ofCssValue((String) currentLexeme.getValue()) != null) { info.animationPlayState = this.getAnimationPlayState(); } return info; } private PXPaint parseSinglePaint() { PXPaint result = null; if (isInTypeSet(PAINT_SET)) { if (isType(PXStylesheetTokenType.LINEAR_GRADIENT)) { result = getLinearGradient(); } else if (isType(PXStylesheetTokenType.RADIAL_GRADIENT)) { result = getRadialGradient(); } else if (isType(PXStylesheetTokenType.URL)) { result = new PXImagePaint(parseURL()); } else if (isInTypeSet(COLOR_SET)) { result = new PXSolidPaint(getColor()); } else { exceptionWithMessage("Unsupported paint token type"); } if (isType(PXStylesheetTokenType.IDENTIFIER) && !isSVGColorName()) { String blendingMode = (String) currentLexeme.getValue(); @SuppressWarnings("rawtypes") Enum blendModeValue = BLEND_MODE_MAP.get(blendingMode); if (blendModeValue instanceof PorterDuff.Mode) { // TODO this is the only one we have so far (Porter Duff, // that is) result.setBleningMode(new PorterDuffXfermode((PorterDuff.Mode) blendModeValue)); } advance(); } } else { exceptionWithMessage("Unrecognized paint token type"); } return result; } private PXAnimationInfo parseTransitionInfo() { PXAnimationInfo info = new PXAnimationInfo(); if (isType(PXStylesheetTokenType.IDENTIFIER) && PXAnimationTimingFunction.ofCssValue((String) currentLexeme.getValue()) == null) { info.animationName = (String) currentLexeme.getValue(); advance(); } if (isType(PXStylesheetTokenType.TIME)) { info.animationDuration = this.getSecondsValue(); } if (isType(PXStylesheetTokenType.IDENTIFIER) && PXAnimationTimingFunction.ofCssValue((String) currentLexeme.getValue()) != null) { info.animationTimingFunction = this.getAnimationTimingFunction(); } if (isType(PXStylesheetTokenType.TIME)) { info.animationDelay = this.getSecondsValue(); } return info; } private Uri parseURL() { Uri result = null; try { if (isType(PXStylesheetTokenType.IDENTIFIER)) { advance(); } else { assertType(PXStylesheetTokenType.URL); String path = (String) currentLexeme.getValue(); advance(); result = Uri.parse(path.replace(" ", "%20")); } } catch (Exception e) { addError(e.getMessage()); } return result; } private PXLinearGradient getLinearGradient() { PXLinearGradient result = new PXLinearGradient(); assertTypeAndAdvance(PXStylesheetTokenType.LINEAR_GRADIENT); if (isType(PXStylesheetTokenType.ANGLE)) { result.setCssAngle(getAngleValue()); // Skip optional comma advanceIfIsType(PXStylesheetTokenType.COMMA); } else if (isIdentifierWithName("to")) { // advance over 'to' advance(); if (isType(PXStylesheetTokenType.IDENTIFIER)) { String text = (String) currentLexeme.getValue(); if ("left".equals(text)) { advance(); if (isIdentifierWithName("top")) { // advance over 'top' advance(); result.setGradientDirection(PXLinearGradientDirection.TO_TOP_LEFT); } else if (isIdentifierWithName("bottom")) { // advance over 'bottom' advance(); result.setGradientDirection(PXLinearGradientDirection.TO_BOTTOM_LEFT); } else { result.setGradientDirection(PXLinearGradientDirection.TO_LEFT); } } else if ("right".equals(text)) { advance(); if (isIdentifierWithName("top")) { // advance over 'top' advance(); result.setGradientDirection(PXLinearGradientDirection.TO_TOP_RIGHT); } else if (isIdentifierWithName("bottom")) { // advance over 'bottom' advance(); result.setGradientDirection(PXLinearGradientDirection.TO_BOTTOM_RIGHT); } else { result.setGradientDirection(PXLinearGradientDirection.TO_RIGHT); } } else if ("top".equals(text)) { advance(); if (isIdentifierWithName("left")) { // advance over 'left' advance(); result.setGradientDirection(PXLinearGradientDirection.TO_TOP_LEFT); } else if (isIdentifierWithName("right")) { // advance over 'right' advance(); result.setGradientDirection(PXLinearGradientDirection.TO_TOP_RIGHT); } else { result.setGradientDirection(PXLinearGradientDirection.TO_TOP); } } else if ("bottom".equals(text)) { advance(); if (isIdentifierWithName("left")) { // advance over 'left' advance(); result.setGradientDirection(PXLinearGradientDirection.TO_BOTTOM_LEFT); } else if (isIdentifierWithName("right")) { // advance over 'right' advance(); result.setGradientDirection(PXLinearGradientDirection.TO_BOTTOM_RIGHT); } else { result.setGradientDirection(PXLinearGradientDirection.TO_BOTTOM); } } else { exceptionWithMessage("Expected 'top', 'right', 'bottom', or 'left' keyword " + "after 'to' when defining a linear gradient angle"); } // Skip optional comma advanceIfIsType(PXStylesheetTokenType.COMMA); } else { exceptionWithMessage("Expected 'top', 'right', 'bottom', or 'left' keyword " + "after 'to' when defining a linear gradient angle"); } } // collect colors do { int color = getColor(); if (isType(PXStylesheetTokenType.PERCENTAGE)) { PXDimension percent = (PXDimension) currentLexeme.getValue(); float offset = percent.getNumber() / 100.0f; advance(); result.addColor(color, offset); } else { result.addColor(color); } // Skip optional comma advanceIfIsType(PXStylesheetTokenType.COMMA); } while (isInTypeSet(COLOR_SET)); // advance over ')' advanceIfIsType(PXStylesheetTokenType.RPAREN); return result; } private PXRadialGradient getRadialGradient() { PXRadialGradient result = new PXRadialGradient(); assertTypeAndAdvance(PXStylesheetTokenType.RADIAL_GRADIENT); // Collect colors do { int color = getColor(); if (isType(PXStylesheetTokenType.PERCENTAGE)) { result.addColor(color, getPercentageValue()); } else { result.addColor(color); } // skip optional comma advanceIfIsType(PXStylesheetTokenType.COMMA); } while (isInTypeSet(COLOR_SET)); // Advance over ")" assertTypeAndAdvance(PXStylesheetTokenType.RPAREN); return result; } private float getPercentageValue() { float result = 0.0f; assertType(PXStylesheetTokenType.PERCENTAGE); Object value = currentLexeme.getValue(); if (value instanceof PXDimension) { result = ((PXDimension) value).getNumber() / 100.0f; } else { exceptionWithMessage("PERCENTAGE lexeme did not have PXDimension value"); } advance(); return result; } private float getSecondsValue() { float result = 0.0f; assertType(PXStylesheetTokenType.TIME); Object value = currentLexeme.getValue(); if (value instanceof PXDimension) { PXDimension dimension = (PXDimension) value; if (dimension.isSeconds()) { result = dimension.getNumber(); } else if (dimension.isMilliseconds()) { result = dimension.getNumber() / 1000.0f; } else { exceptionWithMessage(MessageFormat.format("Unrecognized time unit: {0}", dimension)); } } else { exceptionWithMessage("TIME lexeme did not have PXDimension value"); } advance(); return result; } private PXAnimationTimingFunction getAnimationTimingFunction() { PXAnimationTimingFunction result = PXAnimationTimingFunction.UNDEFINED; if (isType(PXStylesheetTokenType.IDENTIFIER)) { String text = (String) currentLexeme.getValue(); try { result = PXAnimationTimingFunction.valueOf(text.toLowerCase()); } catch (IllegalArgumentException e) { // no-op } } else { exceptionWithMessage("Expected identifier for animation timing function"); } advance(); return result; } private float getFloatValue(DisplayMetrics displayMetrics) { Object value = currentLexeme.getValue(); float result = 0.0f; if (value instanceof Number) { result = ((Number) value).floatValue(); } else if (value instanceof String) { result = Float.parseFloat((String) value); } else if (value instanceof PXDimension) { PXDimension dimension = (PXDimension) value; result = dimension.points(displayMetrics).getNumber(); } advance(); return result; } private int getColumnCount() { int result = GridView.AUTO_FIT; PXStylesheetTokenType currentType = currentLexeme.getType(); Object value = currentLexeme.getValue(); // If currentType is a number, that's the number of columns. If it's an // IDENTIFIER, then the only valid value is "auto". If it's anything // else, it's an invalid value and we'll anyway use the default of // "auto". switch (currentType) { case NUMBER: if (value instanceof Number) { result = ((Number) value).intValue(); } else { result = Integer.parseInt((String) value); } break; case IDENTIFIER: // Check is here to just show the warning if applicable. Default // of "auto" is only valid value. if (!("auto".equals(value))) { exceptionWithMessage("'" + value + "' unrecognized as value for column count."); } break; default: exceptionWithMessage("'" + value + "' unrecognized as value for column count."); } advance(); return result; } private int getColumnGap(DisplayMetrics displayMetrics) { Object value = currentLexeme.getValue(); int result = 0; // "normal" PXStylesheetTokenType currentType = currentLexeme.getType(); switch (currentType) { case IDENTIFIER: if (!("normal".equals(value))) { exceptionWithMessage("'" + value + "' unrecognized as value for column gap"); } break; default: if (value instanceof Number) { result = ((Number) value).intValue(); } else if (value instanceof String) { result = Integer.parseInt((String) value); } else if (value instanceof PXDimension) { PXDimension dimension = (PXDimension) value; result = (int) Math.ceil(dimension.points(displayMetrics).getNumber()); } else { exceptionWithMessage("'" + value + "' unrecognized as value for column gap"); } } advance(); return result; } private PXColumnStretchMode getColumnStretchMode() { PXColumnStretchMode result = PXColumnStretchMode.COLUMN_WIDTH; // Android // default. PXStylesheetTokenType currentType = currentLexeme.getType(); Object value = currentLexeme.getValue(); if (currentType != PXStylesheetTokenType.IDENTIFIER) { exceptionWithMessage("Expected a column stretch mode identifier. '" + value + "' is unrecognized."); } else { result = PXColumnStretchMode.ofCssValue((String) value); if (result == null) { exceptionWithMessage("Expected a column stretch mode identifier. '" + value + "' is unrecognized."); } } advance(); return result; } private int getRowGap(DisplayMetrics displayMetrics) { Object value = currentLexeme.getValue(); int result = 0; // "normal" PXStylesheetTokenType currentType = currentLexeme.getType(); switch (currentType) { case IDENTIFIER: if (!("normal".equals(value))) { exceptionWithMessage("'" + value + "' unrecognized as value for row gap"); } break; default: if (value instanceof Number) { result = ((Number) value).intValue(); } else if (value instanceof String) { result = Integer.parseInt((String) value); } else if (value instanceof PXDimension) { PXDimension dimension = (PXDimension) value; result = (int) Math.ceil(dimension.points(displayMetrics).getNumber()); } else { exceptionWithMessage("'" + value + "' unrecognized as value for row gap"); } } advance(); return result; } private float getColumnWidth(DisplayMetrics displayMetrics) { Object value = currentLexeme.getValue(); float result = 0.0f; // "auto" PXStylesheetTokenType currentType = currentLexeme.getType(); switch (currentType) { case IDENTIFIER: if (!("auto".equals(value))) { exceptionWithMessage("'" + value + "' unrecognized as value for column width"); } break; default: if (value instanceof Number) { result = ((Number) value).floatValue(); } else if (value instanceof String) { result = Float.parseFloat((String) value); } else if (value instanceof PXDimension) { PXDimension dimension = (PXDimension) value; result = dimension.points(displayMetrics).getNumber(); } } advance(); return result; } private Integer getColor() { Integer result = null; PXStylesheetTokenType currentType = currentLexeme.getType(); switch (currentType) { case RGB: advance(); int red = (int) readByteOrPercent(1.0f), green = (int) readByteOrPercent(1.0f), blue = (int) readByteOrPercent(1.0f); result = Color.argb(255, red, green, blue); assertTypeAndAdvance(PXStylesheetTokenType.RPAREN); break; case RGBA: { float r, g, b, a; advance(); if (isType(PXStylesheetTokenType.HEX_COLOR) || isType(PXStylesheetTokenType.IDENTIFIER)) { int color = Color.parseColor((String) currentLexeme.getValue()); r = Color.red(color); g = Color.green(color); b = Color.blue(color); a = Color.alpha(color); advance(); advanceIfIsType(PXStylesheetTokenType.COMMA); } else { r = readByteOrPercent(1.0f); g = readByteOrPercent(1.0f); b = readByteOrPercent(1.0f); a = readByteOrPercent(1.0f); } result = Color.argb((int) a, (int) r, (int) g, (int) b); assertTypeAndAdvance(PXStylesheetTokenType.RPAREN); break; } case HSL: { advance(); float hue = readAngle(), saturation = readByteOrPercent(255.0f), lightness = readByteOrPercent(255.0f); // TODO HSL -> argb calculation. Here we're pretending we // actually // got HSV. CHANGE THIS result = Color.HSVToColor(new float[] { hue, saturation, lightness }); assertTypeAndAdvance(PXStylesheetTokenType.RPAREN); break; } case HSLA: { advance(); float hue = readAngle(), saturation = readByteOrPercent(255.0f), lightness = readByteOrPercent(255.0f), alpha = readByteOrPercent(1.0f); // TODO HSLA -> argb calculation. Here we're pretending we // actually // got HSV. CHANGE THIS result = Color.HSVToColor((int) (alpha * 255.0f), new float[] { hue, saturation, lightness }); assertTypeAndAdvance(PXStylesheetTokenType.RPAREN); break; } case HSB: { advance(); float hue = readAngle(), saturation = readByteOrPercent(255.0f), brightness = readByteOrPercent(255.0f); result = Color.HSVToColor(new float[] { hue, saturation, brightness }); assertTypeAndAdvance(PXStylesheetTokenType.RPAREN); break; } case HSBA: { advance(); float hue = readAngle(), saturation = readByteOrPercent(255.0f), brightness = readByteOrPercent(255.0f), alpha = readByteOrPercent(1.0f); result = Color.HSVToColor((int) (alpha * 255.0f), new float[] { hue, saturation, brightness }); assertTypeAndAdvance(PXStylesheetTokenType.RPAREN); break; } case ID: result = Color.parseColor((String) currentLexeme.getValue()); advance(); break; case IDENTIFIER: result = SVGColors.get((String) currentLexeme.getValue()); advance(); break; default: exceptionWithMessage("Expected RGB, RGBA, HSB, HSBA, HSL, HSLA, COLOR (hex color), " + "or IDENTIFIER (named color)"); } return result; } private float getAngleValue() { float result = 0.0f; assertType(PXStylesheetTokenType.ANGLE); Object value = currentLexeme.getValue(); if (value instanceof PXDimension) { result = ((PXDimension) value).getNumber(); } else { exceptionWithMessage("ANGLE lexeme did not have PXDimension value"); } advance(); return result; } private PXAnimationDirection getAnimationDirection() { PXAnimationDirection result = PXAnimationDirection.UNDEFINED; if (isType(PXStylesheetTokenType.IDENTIFIER)) { String text = (String) currentLexeme.getValue(); try { result = PXAnimationDirection.valueOf(text.toLowerCase()); } catch (IllegalArgumentException e) { // no-op } } else { exceptionWithMessage("Expected identifier for animation direction"); } advance(); return result; } private PXAnimationFillMode getAnimationFillMode() { PXAnimationFillMode result = PXAnimationFillMode.UNDEFINED; if (isType(PXStylesheetTokenType.IDENTIFIER)) { String text = (String) currentLexeme.getValue(); try { result = PXAnimationFillMode.valueOf(text.toLowerCase()); } catch (IllegalArgumentException e) { // no-op } } else { exceptionWithMessage("Expected identifier for animation fill mode"); } advance(); return result; } private PXAnimationPlayState getAnimationPlayState() { PXAnimationPlayState result = PXAnimationPlayState.UNDEFINED; if (isType(PXStylesheetTokenType.IDENTIFIER)) { String text = (String) currentLexeme.getValue(); try { result = PXAnimationPlayState.valueOf(text.toLowerCase()); } catch (IllegalArgumentException e) { // no-op } } else { exceptionWithMessage("Expected identifier for animation play state"); } advance(); return result; } // read a value from [0,255] or a percentage and return in range [0,1] private float readByteOrPercent(float divisor) { float result = 0.0f; if (isType(PXStylesheetTokenType.NUMBER)) { result = Float.parseFloat((String) currentLexeme.getValue()) / divisor; advance(); } else if (isType(PXStylesheetTokenType.PERCENTAGE)) { float percent = ((PXDimension) currentLexeme.getValue()).getNumber(); result = percent / 100.0f; advance(); } advanceIfIsType(PXStylesheetTokenType.COMMA); return result; } // read a value from [0,360] or an angle and return in range [0,1] private float readAngle() { float result = 0.0f; if (isType(PXStylesheetTokenType.NUMBER)) { result = Float.parseFloat((String) currentLexeme.getValue()) / 360.0f; advance(); } else if (isType(PXStylesheetTokenType.ANGLE)) { PXDimension degrees = ((PXDimension) currentLexeme.getValue()).degrees(); result = degrees.getNumber() / 360.0f; advance(); } advanceIfIsType(PXStylesheetTokenType.COMMA); return result; } private float readNumber(DisplayMetrics displayMetrics) { float result = 0.0f; if (isType(PXStylesheetTokenType.NUMBER)) { result = Float.parseFloat((String) currentLexeme.getValue()); advance(); } else if (isType(PXStylesheetTokenType.LENGTH)) { PXDimension length = (PXDimension) currentLexeme.getValue(); result = length.points(displayMetrics).getNumber(); advance(); } advanceIfIsType(PXStylesheetTokenType.COMMA); return result; } private void assertType(PXStylesheetTokenType tokenType) { if (!isType(tokenType)) { exceptionWithMessage("Expected a " + tokenType.name() + " token"); } } private void exceptionWithMessage(String message) { throw new RuntimeException("Unexpected token type. " + message + ". Found " + (currentLexeme != null ? currentLexeme.getTypeName() : "null")); } }