/*******************************************************************************
* 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.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Stack;
import android.content.Context;
import com.pixate.freestyle.cg.math.PXDimension;
import com.pixate.freestyle.parsing.Lexeme;
import com.pixate.freestyle.parsing.PXParserBase;
import com.pixate.freestyle.styling.PXDeclaration;
import com.pixate.freestyle.styling.PXRuleSet;
import com.pixate.freestyle.styling.PXStylesheet;
import com.pixate.freestyle.styling.PXStylesheet.PXStyleSheetOrigin;
import com.pixate.freestyle.styling.animation.PXKeyframe;
import com.pixate.freestyle.styling.animation.PXKeyframeBlock;
import com.pixate.freestyle.styling.combinators.PXAdjacentSiblingCombinator;
import com.pixate.freestyle.styling.combinators.PXChildCombinator;
import com.pixate.freestyle.styling.combinators.PXCombinatorBase;
import com.pixate.freestyle.styling.combinators.PXDescendantCombinator;
import com.pixate.freestyle.styling.combinators.PXSiblingCombinator;
import com.pixate.freestyle.styling.media.PXMediaExpression;
import com.pixate.freestyle.styling.media.PXMediaExpressionGroup;
import com.pixate.freestyle.styling.media.PXNamedMediaExpression;
import com.pixate.freestyle.styling.selectors.PXAttributeSelector;
import com.pixate.freestyle.styling.selectors.PXAttributeSelectorOperator;
import com.pixate.freestyle.styling.selectors.PXClassSelector;
import com.pixate.freestyle.styling.selectors.PXIdSelector;
import com.pixate.freestyle.styling.selectors.PXNotPseudoClass;
import com.pixate.freestyle.styling.selectors.PXPseudoClassFunction;
import com.pixate.freestyle.styling.selectors.PXPseudoClassPredicate;
import com.pixate.freestyle.styling.selectors.PXPseudoClassSelector;
import com.pixate.freestyle.styling.selectors.PXSelector;
import com.pixate.freestyle.styling.selectors.PXTypeSelector;
import com.pixate.freestyle.styling.selectors.PXAttributeSelectorOperator.PXAttributeSelectorOperatorType;
import com.pixate.freestyle.styling.selectors.PXPseudoClassFunction.PXPseudoClassFunctionType;
import com.pixate.freestyle.styling.selectors.PXPseudoClassPredicate.PXPseudoClassPredicateType;
import com.pixate.freestyle.util.CollectionUtil;
import com.pixate.freestyle.util.IOUtil;
import com.pixate.freestyle.util.PXLog;
import com.pixate.freestyle.util.StringUtil;
/**
* Pixate stylesheet parser.
*/
public class PXStylesheetParser extends PXParserBase<PXStylesheetTokenType> {
private static String TAG = PXStylesheetParser.class.getSimpleName();
private static EnumSet<PXStylesheetTokenType> SELECTOR_SEQUENCE_SET;
private static EnumSet<PXStylesheetTokenType> SELECTOR_OPERATOR_SET;
private static EnumSet<PXStylesheetTokenType> SELECTOR_SET;
private static EnumSet<PXStylesheetTokenType> TYPE_SELECTOR_SET;
private static EnumSet<PXStylesheetTokenType> SELECTOR_EXPRESSION_SET;
private static EnumSet<PXStylesheetTokenType> TYPE_NAME_SET;
private static EnumSet<PXStylesheetTokenType> ATTRIBUTE_OPERATOR_SET;
private static EnumSet<PXStylesheetTokenType> DECLARATION_DELIMITER_SET;
private static EnumSet<PXStylesheetTokenType> KEYFRAME_SELECTOR_SET;
private static EnumSet<PXStylesheetTokenType> NAMESPACE_SET;
private static EnumSet<PXStylesheetTokenType> IMPORT_SET;
private static EnumSet<PXStylesheetTokenType> QUERY_VALUE_SET;
private static EnumSet<PXStylesheetTokenType> ARCHAIC_PSEUDO_ELEMENTS_SET;
//@formatter:off
static {
TYPE_NAME_SET = EnumSet.of(
PXStylesheetTokenType.IDENTIFIER,
PXStylesheetTokenType.STAR);
TYPE_SELECTOR_SET = EnumSet.of(PXStylesheetTokenType.PIPE);
TYPE_SELECTOR_SET.addAll(TYPE_NAME_SET);
SELECTOR_EXPRESSION_SET = EnumSet.of(
PXStylesheetTokenType.ID,
PXStylesheetTokenType.CLASS,
PXStylesheetTokenType.LBRACKET,
PXStylesheetTokenType.COLON,
PXStylesheetTokenType.NOT_PSEUDO_CLASS,
PXStylesheetTokenType.LINK_PSEUDO_CLASS,
PXStylesheetTokenType.VISITED_PSEUDO_CLASS,
PXStylesheetTokenType.HOVER_PSEUDO_CLASS,
PXStylesheetTokenType.ACTIVE_PSEUDO_CLASS,
PXStylesheetTokenType.FOCUS_PSEUDO_CLASS,
PXStylesheetTokenType.TARGET_PSEUDO_CLASS,
PXStylesheetTokenType.LANG_PSEUDO_CLASS,
PXStylesheetTokenType.ENABLED_PSEUDO_CLASS,
PXStylesheetTokenType.CHECKED_PSEUDO_CLASS,
PXStylesheetTokenType.INDETERMINATE_PSEUDO_CLASS,
PXStylesheetTokenType.ROOT_PSEUDO_CLASS,
PXStylesheetTokenType.NTH_CHILD_PSEUDO_CLASS,
PXStylesheetTokenType.NTH_LAST_CHILD_PSEUDO_CLASS,
PXStylesheetTokenType.NTH_OF_TYPE_PSEUDO_CLASS,
PXStylesheetTokenType.NTH_LAST_OF_TYPE_PSEUDO_CLASS,
PXStylesheetTokenType.FIRST_CHILD_PSEUDO_CLASS,
PXStylesheetTokenType.LAST_CHILD_PSEUDO_CLASS,
PXStylesheetTokenType.FIRST_OF_TYPE_PSEUDO_CLASS,
PXStylesheetTokenType.LAST_OF_TYPE_PSEUDO_CLASS,
PXStylesheetTokenType.ONLY_CHILD_PSEUDO_CLASS,
PXStylesheetTokenType.ONLY_OF_TYPE_PSEUDO_CLASS,
PXStylesheetTokenType.EMPTY_PSEUDO_CLASS);
SELECTOR_OPERATOR_SET = EnumSet.of(
PXStylesheetTokenType.PLUS,
PXStylesheetTokenType.GREATER_THAN,
PXStylesheetTokenType.TILDE);
SELECTOR_SEQUENCE_SET = EnumSet.of(
PXStylesheetTokenType.PIPE,
PXStylesheetTokenType.IDENTIFIER,
PXStylesheetTokenType.STAR);
SELECTOR_SEQUENCE_SET.addAll(SELECTOR_EXPRESSION_SET);
SELECTOR_SEQUENCE_SET.addAll(SELECTOR_OPERATOR_SET);
SELECTOR_SET = EnumSet.of(PXStylesheetTokenType.PIPE);
SELECTOR_SET.addAll(TYPE_NAME_SET);
SELECTOR_SET.addAll(SELECTOR_EXPRESSION_SET);
ATTRIBUTE_OPERATOR_SET = EnumSet.of(
PXStylesheetTokenType.STARTS_WITH,
PXStylesheetTokenType.ENDS_WITH,
PXStylesheetTokenType.CONTAINS,
PXStylesheetTokenType.EQUAL,
PXStylesheetTokenType.LIST_CONTAINS,
PXStylesheetTokenType.EQUALS_WITH_HYPHEN);
DECLARATION_DELIMITER_SET = EnumSet.of(
PXStylesheetTokenType.SEMICOLON,
PXStylesheetTokenType.RCURLY);
KEYFRAME_SELECTOR_SET = EnumSet.of(
PXStylesheetTokenType.IDENTIFIER,
PXStylesheetTokenType.PERCENTAGE);
NAMESPACE_SET = EnumSet.of(
PXStylesheetTokenType.STRING,
PXStylesheetTokenType.URL);
IMPORT_SET = EnumSet.of(
PXStylesheetTokenType.STRING,
PXStylesheetTokenType.URL);
QUERY_VALUE_SET = EnumSet.of(
PXStylesheetTokenType.IDENTIFIER,
PXStylesheetTokenType.NUMBER,
PXStylesheetTokenType.LENGTH,
PXStylesheetTokenType.STRING);
ARCHAIC_PSEUDO_ELEMENTS_SET = EnumSet.of(
PXStylesheetTokenType.FIRST_LINE_PSEUDO_ELEMENT,
PXStylesheetTokenType.FIRST_LETTER_PSEUDO_ELEMENT,
PXStylesheetTokenType.BEFORE_PSEUDO_ELEMENT,
PXStylesheetTokenType.AFTER_PSEUDO_ELEMENT);
}
//@formatter:on
private PXStylesheetLexer lexer;
private PXStylesheet currentStyleSheet;
private Stack<String> activeImports;
private Stack<PXStylesheetLexer> lexerStack;
// Application context
private Context context;
/**
* Constructs a new parser.
*/
public PXStylesheetParser() {
this(null);
}
/**
* Constructs a new parser with a {@link Context}.
*
* @param context
*/
public PXStylesheetParser(Context context) {
lexer = new PXStylesheetLexer();
this.context = context;
}
/**
* Sets the {@link Context} that will be used by this parser (e.g. the
* application context).
*
* @param context
*/
public void setContext(Context context) {
this.context = context;
}
/**
* Returns the {@link Context} that is used by this parser.
*
* @return A {@link Context}
*/
public Context getContext() {
return context;
}
/**
* Parse the style-sheet.
*
* @param source
* @param origin
* @param fileName
* @return
*/
public PXStylesheet parse(String source, PXStyleSheetOrigin origin, String fileName) {
// add the source file name to prevent @imports from importing it as
// well
addImportName(fileName);
// parse
PXStylesheet result = parse(source, origin);
// associate file path on resulting stylesheet
result.setFilePath(fileName);
return result;
}
/**
* Parse the style-sheet.
*
* @param source
* @param origin
* @return
*/
public PXStylesheet parse(String source, PXStyleSheetOrigin origin) {
// clear errors
clearErrors();
// create stylesheet
currentStyleSheet = new PXStylesheet(origin);
// setup lexer and prime it
lexer.setSource(source);
advance();
try {
while (currentLexeme != null && currentLexeme.getType() != PXStylesheetTokenType.EOF) {
switch (currentLexeme.getType()) {
case IMPORT:
parseImport();
break;
case NAMESPACE:
parseNamespace();
break;
case KEYFRAMES:
parseKeyframes();
break;
case MEDIA:
parseMedia();
break;
case FONT_FACE:
parseFontFace();
break;
default:
// TODO: check for valid tokens to error out sooner?
parseRuleSet();
break;
}
}
} catch (Exception e) {
addError(e.getMessage());
}
// clear out any import refs
activeImports = null;
return currentStyleSheet;
}
public PXStylesheet parseInlineCSS(String css) {
// clear errors
clearErrors();
// create stylesheet
currentStyleSheet = new PXStylesheet(PXStyleSheetOrigin.INLINE);
// setup lexer and prime it
lexer.setSource(css);
advance();
try {
// build placeholder rule set
PXRuleSet ruleSet = new PXRuleSet();
// parse declarations
List<PXDeclaration> declarations = parseDeclarations();
// add declarations to rule set
for (PXDeclaration declaration : declarations) {
ruleSet.addDeclaration(declaration);
}
// save rule set
currentStyleSheet.addRuleSet(ruleSet);
} catch (Exception e) {
addError(e.getMessage());
}
return currentStyleSheet;
}
@SuppressWarnings("unused")
/* Unused, consider deletion. */
private PXSelector parseSelectorString(String source) {
// clear errors
clearErrors();
// setup lexer and prime it
lexer.setSource(source);
advance();
try {
return parseSelector();
} catch (Exception e) {
addError(e.getMessage());
}
return null;
}
// level 1
private void parseFontFace() {
assertTypeAndAdvance(PXStylesheetTokenType.FONT_FACE);
// process declaration block
if (isType(PXStylesheetTokenType.LCURLY)) {
List<PXDeclaration> declarations = parseDeclarationBlock();
// TODO: we probably shouldn't load font right here
for (PXDeclaration declaration : declarations) {
if ("src".equals(declaration.getName())) {
// Load a font and hold it in the fonts registry
// Shalom FIXME - We need access to the application's
// AssetManager!
// PXFontRegistry.getTypeface(declaration.getURLValue());
}
}
}
}
private void parseImport() {
assertTypeAndAdvance(PXStylesheetTokenType.IMPORT);
assertTypeInSet(IMPORT_SET);
String path = null;
switch (currentLexeme.getType()) {
case STRING: {
String string = currentLexeme.getValue().toString();
if (string.length() > 2) {
path = string.substring(1, string.length() - 2);
}
break;
}
case URL:
path = currentLexeme.getValue().toString();
break;
default:
break;
}
if (path != null) {
// advance over @import argument
advance();
// calculate resource name and file extension
// int dotIndex = path.lastIndexOf(".");
// String pathMinusExtension = dotIndex > -1 ? path.substring(0,
// dotIndex) : path;
// String extension = dotIndex > -1 ? path.substring(dotIndex +
// 1).toLowerCase()
// : StringUtil.EMPTY;
if (context == null) {
addError("Error parsing an import. The application context is null.");
advance();
} else if (!activeImports.contains(path)) {
// we need to go ahead and process the trailing semicolon so we
// have the current lexeme in case we push it below
advance();
addImportName(path);
// Note: We always take the import css from the assets.
String source = null;
try {
source = IOUtil.read(context.getAssets().open(path));
} catch (IOException e) {
PXLog.e(TAG, e, e.getMessage());
}
if (!StringUtil.isEmpty(source)) {
lexer.pushLexeme((PXStylesheetLexeme) currentLexeme);
pushSource(source);
advance();
}
} else {
String message = String.format(
"import cycle detected trying to import '%s':\n%s ->\n%s", path,
CollectionUtil.toString(activeImports, " ->\n"), path);
addError(message);
// NOTE: we do this here so we'll still have the current file on
// the active imports stack. This handles the
// case of a file ending with an @import statement, causing
// advance to pop it from the active imports stack
advance();
}
}
}
private void parseMedia() {
assertTypeAndAdvance(PXStylesheetTokenType.MEDIA);
// TODO: support media types, NOT, and ONLY. Skipping for now
while (isType(PXStylesheetTokenType.IDENTIFIER)) {
advance();
}
// 'and' may appear here
advanceIfIsType(PXStylesheetTokenType.AND);
// parse optional expressions
if (isType(PXStylesheetTokenType.LPAREN)) {
parseMediaExpressions();
}
// parse body
if (isType(PXStylesheetTokenType.LCURLY)) {
try {
advance();
while (currentLexeme != null
&& currentLexeme.getType() != PXStylesheetTokenType.EOF
&& !isType(PXStylesheetTokenType.RCURLY)) {
parseRuleSet();
}
advanceIfIsType(PXStylesheetTokenType.RCURLY,
"Expected @media body closing curly brace");
} finally {
// reset active media query to none
currentStyleSheet.setActiveMediaQuery(null);
}
}
}
private void parseRuleSet() {
List<PXSelector> selectors;
// parse selectors
try {
selectors = parseSelectorGroup();
} catch (Exception e) {
// emit error
addError(e.getMessage());
// use flag to indicate we have no selectors
selectors = null;
// advance to '{'
advanceToType(PXStylesheetTokenType.LCURLY);
}
// here for error recovery
if (!isType(PXStylesheetTokenType.LCURLY)) {
addError("Expected a left curly brace to begin a declaration block");
// advance to '{'
advanceToType(PXStylesheetTokenType.LCURLY);
}
// parse declaration block
if (isType(PXStylesheetTokenType.LCURLY)) {
List<PXDeclaration> declarations = parseDeclarationBlock();
if (selectors == null) {
PXRuleSet ruleSet = new PXRuleSet();
for (PXDeclaration declaration : declarations) {
ruleSet.addDeclaration(declaration);
}
// save rule set
currentStyleSheet.addRuleSet(ruleSet);
} else {
for (PXSelector selector : selectors) {
// build rule set
PXRuleSet ruleSet = new PXRuleSet();
// add selector
if (selector != null) {
ruleSet.addSelector(selector);
}
for (PXDeclaration declaration : declarations) {
ruleSet.addDeclaration(declaration);
}
// save rule set
currentStyleSheet.addRuleSet(ruleSet);
}
}
}
}
private void parseKeyframes() {
// advance over '@keyframes'
assertTypeAndAdvance(PXStylesheetTokenType.KEYFRAMES);
// grab keyframe name
assertType(PXStylesheetTokenType.IDENTIFIER);
PXKeyframe keyframe = new PXKeyframe(currentLexeme.getValue().toString());
advance();
// advance over '{'
assertTypeAndAdvance(PXStylesheetTokenType.LCURLY);
// process each block
while (isInTypeSet(KEYFRAME_SELECTOR_SET)) {
// grab all offsets
List<Number> offsets = new ArrayList<Number>();
offsets.add(parseOffset());
while (isType(PXStylesheetTokenType.COMMA)) {
// advance over ','
advance();
offsets.add(parseOffset());
}
// grab declarations
List<PXDeclaration> declarations = parseDeclarationBlock();
// create blocks, one for each offset, using the same declarations
// for each
for (Number number : offsets) {
float offset = number.floatValue();
// create keyframe block
PXKeyframeBlock block = new PXKeyframeBlock(offset);
// add declarations to it
for (PXDeclaration declaration : declarations) {
block.addDeclaration(declaration);
}
keyframe.addKeyframeBlock(block);
}
}
// add keyframe to current stylesheet
currentStyleSheet.addKeyframe(keyframe);
// advance over '}'
assertTypeAndAdvance(PXStylesheetTokenType.RCURLY);
}
private float parseOffset() {
float offset = 0.0f;
assertTypeInSet(KEYFRAME_SELECTOR_SET);
switch (currentLexeme.getType()) {
case IDENTIFIER:
// NOTE: we only check for 'to' since 'from' and unrecognized
// values will use the default value of 0.0f
if ("to".equals(currentLexeme.getValue())) {
offset = 1.0f;
}
advance();
break;
case PERCENTAGE: {
PXDimension percentage = (PXDimension) currentLexeme.getValue();
offset = percentage.getNumber() / 100.0f;
offset = Math.min(1.0f, offset);
offset = Math.max(0.0f, offset);
advance();
break;
}
default: {
String message = String.format("Unrecognized keyframe selector type: %s",
currentLexeme);
errorWithMessage(message);
break;
}
}
return offset;
}
private void parseNamespace() {
assertTypeAndAdvance(PXStylesheetTokenType.NAMESPACE);
String identifier = null;
String uri;
if (isType(PXStylesheetTokenType.IDENTIFIER)) {
identifier = currentLexeme.getValue().toString();
advance();
}
assertTypeInSet(NAMESPACE_SET);
// grab value
uri = currentLexeme.getValue().toString();
// trim string
if (isType(PXStylesheetTokenType.STRING)) {
// this will remove the URI double quotes.
uri = uri.substring(1, uri.length() - 1);
}
advance();
// set namespace on stylesheet (identifier is the namespace prefix)
currentStyleSheet.setURI(uri, identifier);
assertTypeAndAdvance(PXStylesheetTokenType.SEMICOLON);
}
// level 2
private List<PXSelector> parseSelectorGroup() {
List<PXSelector> selectors = new ArrayList<PXSelector>();
PXSelector selectorSequence = parseSelectorSequence();
if (selectorSequence != null) {
selectors.add(selectorSequence);
}
while (currentLexeme.getType() == PXStylesheetTokenType.COMMA) {
// advance over ','
advance();
// grab next selector
PXSelector nextSelector = parseSelectorSequence();
if (nextSelector == null) {
// We have a problem with this selectors group
errorWithMessage("Expected a Selector or Pseudo-element after a comma");
} else {
selectors.add(nextSelector);
}
}
if (selectors.size() == 0) {
errorWithMessage("Expected a Selector or Pseudo-element");
}
return selectors;
}
private List<PXDeclaration> parseDeclarationBlock() {
assertTypeAndAdvance(PXStylesheetTokenType.LCURLY);
List<PXDeclaration> declarations = parseDeclarations();
assertTypeAndAdvance(PXStylesheetTokenType.RCURLY);
return declarations;
}
private void parseMediaExpressions() {
try {
// create container for zero-or-more expressions
List<PXMediaExpression> expressions = new ArrayList<PXMediaExpression>();
// add at least one expression
expressions.add(parseMediaExpression());
// and any others
while (isType(PXStylesheetTokenType.AND)) {
advance();
expressions.add(parseMediaExpression());
}
// create expression group or use single entry
if (expressions.size() == 1) {
currentStyleSheet.setActiveMediaQuery(expressions.get(0));
} else {
PXMediaExpressionGroup group = new PXMediaExpressionGroup();
for (PXMediaExpression expression : expressions) {
group.addExpression(expression);
}
currentStyleSheet.setActiveMediaQuery(group);
}
} catch (Exception e) {
addError(e.getMessage());
// TODO: error recovery
}
}
// level 3
private PXSelector parseSelectorSequence() {
PXSelector root = parseSelector();
while (isInTypeSet(SELECTOR_SEQUENCE_SET)) {
Lexeme<PXStylesheetTokenType> operator = null;
if (isInTypeSet(SELECTOR_OPERATOR_SET)) {
operator = currentLexeme;
advance();
}
PXSelector rhs = parseSelector();
if (operator != null) {
switch (operator.getType()) {
case PLUS:
root = new PXAdjacentSiblingCombinator(root, rhs);
break;
case GREATER_THAN:
root = new PXChildCombinator(root, rhs);
break;
case TILDE:
root = new PXSiblingCombinator(root, rhs);
break;
default:
errorWithMessage("Unsupported selector operator (combinator)");
}
} else {
root = new PXDescendantCombinator(root, rhs);
// advance();
}
}
String pseudoElement = null;
// grab possible pseudo-element in new and old formats
if (isType(PXStylesheetTokenType.DOUBLE_COLON)) {
advance();
assertType(PXStylesheetTokenType.IDENTIFIER);
pseudoElement = currentLexeme.getValue().toString();
advance();
} else if (isInTypeSet(ARCHAIC_PSEUDO_ELEMENTS_SET)) {
String stringValue = currentLexeme.getValue().toString();
pseudoElement = stringValue.substring(1);
advance();
}
if (pseudoElement != null && pseudoElement.length() > 0) {
if (root == null) {
PXTypeSelector selector = new PXTypeSelector();
selector.setPseudoElement(pseudoElement);
root = selector;
} else {
if (root instanceof PXTypeSelector) {
PXTypeSelector selector = (PXTypeSelector) root;
selector.setPseudoElement(pseudoElement);
} else if (root instanceof PXCombinatorBase) {
PXCombinatorBase combinator = (PXCombinatorBase) root;
PXTypeSelector selector = (PXTypeSelector) combinator.getRhs();
selector.setPseudoElement(pseudoElement);
}
}
}
return root;
}
private List<PXDeclaration> parseDeclarations() {
List<PXDeclaration> declarations = new ArrayList<PXDeclaration>();
// parse properties
while (currentLexeme != null && currentLexeme.getType() != PXStylesheetTokenType.EOF
&& currentLexeme.getType() != PXStylesheetTokenType.RCURLY) {
try {
PXDeclaration declaration = parseDeclaration();
declarations.add(declaration);
} catch (Exception e) {
addError(e.getMessage());
// TODO: parseDeclaration could do error recovery. If not, this
// should probably do the same recovery
while (currentLexeme != null
&& currentLexeme.getType() != PXStylesheetTokenType.EOF
&& !isInTypeSet(DECLARATION_DELIMITER_SET)) {
advance();
}
advanceIfIsType(PXStylesheetTokenType.SEMICOLON);
}
}
return declarations;
}
private PXMediaExpression parseMediaExpression() {
assertTypeAndAdvance(PXStylesheetTokenType.LPAREN);
// grab name
assertType(PXStylesheetTokenType.IDENTIFIER);
String name = currentLexeme.getValue().toString().toLowerCase(Locale.US);
advance();
Object value = null;
// parse optional value
if (isType(PXStylesheetTokenType.COLON)) {
// advance over ':'
assertTypeAndAdvance(PXStylesheetTokenType.COLON);
// grab value
assertTypeInSet(QUERY_VALUE_SET);
value = currentLexeme.getValue();
boolean isNumber = currentLexeme.getType() == PXStylesheetTokenType.NUMBER;
advance();
// make string values lowercase to avoid doing it later
if (!isNumber && value instanceof String) {
value = ((String) value).toLowerCase(Locale.US);
}
// check for possible ratio syntax
else if (isNumber && isType(PXStylesheetTokenType.SLASH)) {
Float numerator = getFloatValue(value);
// advance over '/'
advance();
// grab denominator
assertType(PXStylesheetTokenType.NUMBER);
Float denom = getFloatValue(currentLexeme.getValue());
advance();
if (numerator.floatValue() == 0.0f) {
// do nothing, leave result as 0.0
} else if (denom.floatValue() == 0.0f) {
value = Double.NaN;
} else {
value = numerator.floatValue() / denom.floatValue();
}
}
}
advanceIfIsType(PXStylesheetTokenType.RPAREN, "Expected closing parenthesis in media query");
// create query expression and activate it in current stylesheet
return new PXNamedMediaExpression(name, value);
}
/**
* Tries to convert the given value into a {@link Float}. In case the value
* is already instance of Number, the method simply cast it to one.
*
* @param value
* @return A {@link Float} instance; <code>null</code> in case the value
* cannot be converted.
*/
private Float getFloatValue(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number) {
return ((Number) value).floatValue();
}
try {
return Float.parseFloat(value.toString());
} catch (NumberFormatException e) {
return null;
}
}
// level 4
private PXSelector parseSelector() {
PXTypeSelector result = null;
if (isInTypeSet(SELECTOR_SET)) {
if (isInTypeSet(TYPE_SELECTOR_SET)) {
result = parseTypeSelector();
} else {
// match any element
result = new PXTypeSelector();
// clear whitespace flag, so first expression will not fail in
// this case
currentLexeme.clearFlag(Lexeme.FLAG_TYPE_FOLLOWS_WHITESPACE);
}
if (isInTypeSet(SELECTOR_EXPRESSION_SET)) {
for (PXSelector expression : parseSelectorExpressions()) {
result.addAttributeExpression(expression);
}
}
}
// else, fail silently in case a pseudo-element follows
return result;
}
private PXDeclaration parseDeclaration() {
// process property name
assertType(PXStylesheetTokenType.IDENTIFIER);
PXDeclaration declaration = new PXDeclaration(currentLexeme.getValue().toString());
advance();
// colon
assertTypeAndAdvance(PXStylesheetTokenType.COLON);
// collect values
Stack<PXStylesheetLexeme> lexemes = new Stack<PXStylesheetLexeme>();
while (currentLexeme != null && currentLexeme.getType() != PXStylesheetTokenType.EOF
&& !isInTypeSet(DECLARATION_DELIMITER_SET)) {
if (!lexemes.isEmpty() && currentLexeme.getType() == PXStylesheetTokenType.COLON
&& lexemes.lastElement().getType() == PXStylesheetTokenType.IDENTIFIER) {
// assume we've moved into a new declaration, so push last
// lexeme back into the lexeme stream
Lexeme<PXStylesheetTokenType> propertyName = lexemes.pop();
// this pushes the colon back to the lexer and makes the
// property name the current lexeme
pushLexeme(propertyName);
// signal end of this declaration
break;
} else {
lexemes.add((PXStylesheetLexeme) currentLexeme);
advance();
}
}
// let semicolons be optional
advanceIfIsType(PXStylesheetTokenType.SEMICOLON);
// grab original source, for error messages and hashing
String source;
if (lexemes.size() > 0) {
Lexeme<PXStylesheetTokenType> firstLexeme = lexemes.firstElement();
Lexeme<PXStylesheetTokenType> lastLexeme = lexemes.firstElement();
int start = firstLexeme.getOffset();
int end = lastLexeme.getEndingOffset();
source = lexer.getSource().substring(start, end);
} else {
source = StringUtil.EMPTY;
}
// check for !important
Lexeme<PXStylesheetTokenType> lastLexeme = lexemes.isEmpty() ? null : lexemes.lastElement();
if (lastLexeme != null && lastLexeme.getType() == PXStylesheetTokenType.IMPORTANT) {
// drop !important and tag declaration as important
lexemes.pop();
declaration.setImportant(true);
}
// associate lexemes with declaration
declaration.setSource(source, getCurrentFilename(), new ArrayList<PXStylesheetLexeme>(
lexemes));
return declaration;
}
// level 5
private PXTypeSelector parseTypeSelector() {
PXTypeSelector result = null;
if (isInTypeSet(TYPE_SELECTOR_SET)) {
String namespace = null;
String name = null;
// namespace or type
if (isInTypeSet(TYPE_NAME_SET)) {
// assume we have a name only
name = currentLexeme.getValue().toString();
advance();
}
// if pipe, then we had a namespace, now process type
if (isType(PXStylesheetTokenType.PIPE)) {
namespace = name;
// advance over '|'
advance();
if (isInTypeSet(TYPE_NAME_SET)) {
// set name
name = currentLexeme.getValue().toString();
advance();
} else {
errorWithMessage("Expected IDENTIFIER or STAR");
}
} else {
namespace = "*";
}
// find namespace URI from namespace prefix
String namespaceURI = null;
if (namespace != null) {
if ("*".equals(namespace)) {
namespaceURI = namespace;
} else {
namespaceURI = currentStyleSheet.getNamespaceForPrefix(namespace);
}
}
result = new PXTypeSelector(namespaceURI, name);
} else {
errorWithMessage("Expected IDENTIFIER, STAR, or PIPE");
}
return result;
}
private List<PXSelector> parseSelectorExpressions() {
List<PXSelector> expressions = new ArrayList<PXSelector>();
while (!currentLexeme.isFlagSet(Lexeme.FLAG_TYPE_FOLLOWS_WHITESPACE)
&& isInTypeSet(SELECTOR_EXPRESSION_SET)) {
switch (currentLexeme.getType()) {
case ID: {
String name = currentLexeme.getValue().toString().substring(1);
expressions.add(new PXIdSelector(name));
advance();
break;
}
case CLASS: {
String name = currentLexeme.getValue().toString().substring(1);
expressions.add(new PXClassSelector(name));
advance();
break;
}
case LBRACKET:
expressions.add(parseAttributeSelector());
break;
case COLON:
expressions.add(parsePseudoClass());
break;
case NOT_PSEUDO_CLASS:
expressions.add(parseNotSelector());
break;
case ROOT_PSEUDO_CLASS:
expressions.add(new PXPseudoClassPredicate(
PXPseudoClassPredicateType.PREDICATE_ROOT));
advance();
break;
case FIRST_CHILD_PSEUDO_CLASS:
expressions.add(new PXPseudoClassPredicate(
PXPseudoClassPredicateType.PREDICATE_FIRST_CHILD));
advance();
break;
case LAST_CHILD_PSEUDO_CLASS:
expressions.add(new PXPseudoClassPredicate(
PXPseudoClassPredicateType.PREDICATE_LAST_CHILD));
advance();
break;
case FIRST_OF_TYPE_PSEUDO_CLASS:
expressions.add(new PXPseudoClassPredicate(
PXPseudoClassPredicateType.PREDICATE_FIRST_OF_TYPE));
advance();
break;
case LAST_OF_TYPE_PSEUDO_CLASS:
expressions.add(new PXPseudoClassPredicate(
PXPseudoClassPredicateType.PREDICATE_LAST_OF_TYPE));
advance();
break;
case ONLY_CHILD_PSEUDO_CLASS:
expressions.add(new PXPseudoClassPredicate(
PXPseudoClassPredicateType.PREDICATE_ONLY_CHILD));
advance();
break;
case ONLY_OF_TYPE_PSEUDO_CLASS:
expressions.add(new PXPseudoClassPredicate(
PXPseudoClassPredicateType.PREDICATE_ONLY_OF_TYPE));
advance();
break;
case EMPTY_PSEUDO_CLASS:
expressions.add(new PXPseudoClassPredicate(
PXPseudoClassPredicateType.PREDICATE_EMPTY));
advance();
break;
case NTH_CHILD_PSEUDO_CLASS:
case NTH_LAST_CHILD_PSEUDO_CLASS:
case NTH_OF_TYPE_PSEUDO_CLASS:
case NTH_LAST_OF_TYPE_PSEUDO_CLASS:
expressions.add(parsePseudoClassFunction());
assertTypeAndAdvance(PXStylesheetTokenType.RPAREN);
break;
// TODO: implement
case LINK_PSEUDO_CLASS:
case VISITED_PSEUDO_CLASS:
case HOVER_PSEUDO_CLASS:
case ACTIVE_PSEUDO_CLASS:
case FOCUS_PSEUDO_CLASS:
case TARGET_PSEUDO_CLASS:
case ENABLED_PSEUDO_CLASS:
case CHECKED_PSEUDO_CLASS:
case INDETERMINATE_PSEUDO_CLASS:
String className = currentLexeme.getValue().toString();
if (className.startsWith(":")) {
className = className.substring(1);
}
expressions.add(new PXPseudoClassSelector(className));
advance();
break;
// TODO: implement
case LANG_PSEUDO_CLASS:
className = currentLexeme.getValue().toString();
if (className.startsWith(":")) {
className = className.substring(1);
}
expressions.add(new PXPseudoClassSelector(className));
advanceToType(PXStylesheetTokenType.RPAREN);
advance();
break;
default:
break;
}
}
if (expressions.size() == 0
&& !currentLexeme.isFlagSet(Lexeme.FLAG_TYPE_FOLLOWS_WHITESPACE)) {
errorWithMessage("Expected ID, CLASS, LBRACKET, or PseudoClass");
}
return expressions;
}
// level 6
private PXPseudoClassFunction parsePseudoClassFunction() {
// initialize to something to remove analyzer warnings, but the switch
// below has to cover all cases to prevent a
// bug here
PXPseudoClassFunctionType type = PXPseudoClassFunctionType.NTH_CHILD;
switch (currentLexeme.getType()) {
case NTH_CHILD_PSEUDO_CLASS:
type = PXPseudoClassFunctionType.NTH_CHILD;
break;
case NTH_LAST_CHILD_PSEUDO_CLASS:
type = PXPseudoClassFunctionType.NTH_LAST_CHILD;
break;
case NTH_OF_TYPE_PSEUDO_CLASS:
type = PXPseudoClassFunctionType.NTH_OF_TYPE;
break;
case NTH_LAST_OF_TYPE_PSEUDO_CLASS:
type = PXPseudoClassFunctionType.NTH_LAST_OF_TYPE;
break;
default:
break;
}
// advance over function name and left paren
advance();
int modulus = 0;
int remainder = 0;
// parse modulus
if (isType(PXStylesheetTokenType.NTH)) {
String numberString = currentLexeme.getValue().toString();
int length = numberString.length();
// extract modulus
if (length == 1) {
// we have 'n'
modulus = 1;
} else if (length == 2 && numberString.startsWith("-")) {
// we have '-n'
modulus = -1;
} else if (length == 2 && numberString.startsWith("+")) {
// we have '+n'
modulus = 1;
} else {
// a number precedes 'n'
modulus = Integer.parseInt(numberString.substring(0, numberString.length() - 1));
}
advance();
if (isType(PXStylesheetTokenType.PLUS)) {
advance();
// grab remainder
assertType(PXStylesheetTokenType.NUMBER);
Number remainderNumber = getFloatValue(currentLexeme.getValue());
remainder = remainderNumber.intValue();
advance();
} else if (isType(PXStylesheetTokenType.NUMBER)) {
numberString = lexer.getSource().substring(currentLexeme.getOffset(),
currentLexeme.getEndingOffset());
if (numberString.startsWith("-") || numberString.startsWith("+")) {
Number remainderNumber = getFloatValue(currentLexeme.getValue());
remainder = remainderNumber.intValue();
advance();
} else {
errorWithMessage("Expected NUMBER with leading '-' or '+'");
}
}
} else if (isType(PXStylesheetTokenType.IDENTIFIER)) {
String stringValue = currentLexeme.getValue().toString();
if ("odd".equals(stringValue)) {
modulus = 2;
remainder = 1;
} else if ("even".equals(stringValue)) {
modulus = 2;
} else {
errorWithMessage(String.format(
"Unrecognized identifier '%s'. Expected 'odd' or 'even'", stringValue));
}
advance();
} else if (isType(PXStylesheetTokenType.NUMBER)) {
modulus = 1;
Number remainderNumber = getFloatValue(currentLexeme.getValue());
remainder = remainderNumber.intValue();
advance();
} else {
errorWithMessage("Expected NTH, NUMBER, 'odd', or 'even'");
}
return new PXPseudoClassFunction(type, modulus, remainder);
}
private PXSelector parseAttributeSelector() {
PXSelector result = null;
assertTypeAndAdvance(PXStylesheetTokenType.LBRACKET);
result = parseAttributeTypeSelector();
if (isInTypeSet(ATTRIBUTE_OPERATOR_SET)) {
PXAttributeSelectorOperatorType operatorType = PXAttributeSelectorOperatorType.EQUAL; // make
// anaylzer
// happy
switch (currentLexeme.getType()) {
case STARTS_WITH:
operatorType = PXAttributeSelectorOperatorType.STARTS_WITH;
break;
case ENDS_WITH:
operatorType = PXAttributeSelectorOperatorType.ENDS_WITH;
break;
case CONTAINS:
operatorType = PXAttributeSelectorOperatorType.CONTAINS;
break;
case EQUAL:
operatorType = PXAttributeSelectorOperatorType.EQUAL;
break;
case LIST_CONTAINS:
operatorType = PXAttributeSelectorOperatorType.LIST_CONTAINS;
break;
case EQUALS_WITH_HYPHEN:
operatorType = PXAttributeSelectorOperatorType.EQUAL_WITH_HYPHEN;
break;
default:
errorWithMessage("Unsupported attribute operator type");
break;
}
advance();
if (isType(PXStylesheetTokenType.STRING)) {
String value = currentLexeme.getValue().toString();
// process string
result = new PXAttributeSelectorOperator(operatorType,
(PXAttributeSelector) result, value.substring(1, value.length() - 1));
advance();
} else if (isType(PXStylesheetTokenType.IDENTIFIER)) {
// process string
result = new PXAttributeSelectorOperator(operatorType,
(PXAttributeSelector) result, currentLexeme.getValue().toString());
advance();
} else {
errorWithMessage("Expected STRING or IDENTIFIER");
}
}
assertTypeAndAdvance(PXStylesheetTokenType.RBRACKET);
return result;
}
private PXSelector parsePseudoClass() {
PXSelector result = null;
assertType(PXStylesheetTokenType.COLON);
advance();
if (isType(PXStylesheetTokenType.IDENTIFIER)) {
// process identifier
result = new PXPseudoClassSelector(currentLexeme.getValue().toString());
advance();
} else {
errorWithMessage("Expected IDENTIFIER");
}
// TODO: support an+b notation
return result;
}
private PXSelector parseNotSelector() {
// advance over 'not'
assertType(PXStylesheetTokenType.NOT_PSEUDO_CLASS);
advance();
PXSelector result = new PXNotPseudoClass(parseNegationArgument());
// advance over ')'
assertTypeAndAdvance(PXStylesheetTokenType.RPAREN);
return result;
}
// level 7
private PXAttributeSelector parseAttributeTypeSelector() {
PXAttributeSelector result = null;
if (isInTypeSet(TYPE_SELECTOR_SET)) {
String namespace = null;
String name = null;
// namespace or type
if (isInTypeSet(TYPE_NAME_SET)) {
// assume we have a name only
name = currentLexeme.getValue().toString();
advance();
}
// if pipe, then we had a namespace, now process type
if (isType(PXStylesheetTokenType.PIPE)) {
namespace = name;
// advance over '|'
advance();
if (isInTypeSet(TYPE_NAME_SET)) {
// set name
name = currentLexeme.getValue().toString();
advance();
} else {
errorWithMessage("Expected IDENTIFIER or STAR");
}
}
// NOTE: default namepace is null indicating no namespace should
// exist when matching with this selector. This
// differs from the interpretation used on type selectors
// find namespace URI from namespace prefix
String namespaceURI = null;
if (namespace != null) {
if (namespace.equals("*")) {
namespaceURI = namespace;
} else {
namespaceURI = currentStyleSheet.getNamespaceForPrefix(namespace);
}
}
result = new PXAttributeSelector(namespaceURI, name);
} else {
errorWithMessage("Expected IDENTIFIER, STAR, or PIPE");
}
return result;
}
private PXSelector parseNegationArgument() {
PXSelector result = null;
switch (currentLexeme.getType()) {
case ID: {
String name = currentLexeme.getValue().toString().substring(1);
result = new PXIdSelector(name);
advance();
break;
}
case CLASS: {
String name = currentLexeme.getValue().toString().substring(1);
result = new PXClassSelector(name);
advance();
break;
}
case LBRACKET:
result = parseAttributeSelector();
break;
case COLON:
result = parsePseudoClass();
break;
case ROOT_PSEUDO_CLASS:
result = new PXPseudoClassPredicate(PXPseudoClassPredicateType.PREDICATE_ROOT);
advance();
break;
case FIRST_CHILD_PSEUDO_CLASS:
result = new PXPseudoClassPredicate(
PXPseudoClassPredicateType.PREDICATE_FIRST_CHILD);
advance();
break;
case LAST_CHILD_PSEUDO_CLASS:
result = new PXPseudoClassPredicate(PXPseudoClassPredicateType.PREDICATE_LAST_CHILD);
advance();
break;
case FIRST_OF_TYPE_PSEUDO_CLASS:
result = new PXPseudoClassPredicate(
PXPseudoClassPredicateType.PREDICATE_FIRST_OF_TYPE);
advance();
break;
case LAST_OF_TYPE_PSEUDO_CLASS:
result = new PXPseudoClassPredicate(
PXPseudoClassPredicateType.PREDICATE_LAST_OF_TYPE);
advance();
break;
case ONLY_CHILD_PSEUDO_CLASS:
result = new PXPseudoClassPredicate(PXPseudoClassPredicateType.PREDICATE_ONLY_CHILD);
advance();
break;
case ONLY_OF_TYPE_PSEUDO_CLASS:
result = new PXPseudoClassPredicate(
PXPseudoClassPredicateType.PREDICATE_ONLY_OF_TYPE);
advance();
break;
case EMPTY_PSEUDO_CLASS:
result = new PXPseudoClassPredicate(PXPseudoClassPredicateType.PREDICATE_EMPTY);
advance();
break;
case NTH_CHILD_PSEUDO_CLASS:
case NTH_LAST_CHILD_PSEUDO_CLASS:
case NTH_OF_TYPE_PSEUDO_CLASS:
case NTH_LAST_OF_TYPE_PSEUDO_CLASS:
result = parsePseudoClassFunction();
assertTypeAndAdvance(PXStylesheetTokenType.RPAREN);
break;
// TODO: implement
case LINK_PSEUDO_CLASS:
case VISITED_PSEUDO_CLASS:
case HOVER_PSEUDO_CLASS:
case ACTIVE_PSEUDO_CLASS:
case FOCUS_PSEUDO_CLASS:
case TARGET_PSEUDO_CLASS:
case ENABLED_PSEUDO_CLASS:
case CHECKED_PSEUDO_CLASS:
case INDETERMINATE_PSEUDO_CLASS:
result = new PXPseudoClassSelector(currentLexeme.getValue().toString());
advance();
break;
// TODO: implement
case LANG_PSEUDO_CLASS:
result = new PXPseudoClassSelector(currentLexeme.getValue().toString());
advanceToType(PXStylesheetTokenType.RPAREN);
advance();
break;
case RPAREN:
// empty body
break;
default:
if (isInTypeSet(TYPE_SELECTOR_SET)) {
result = parseTypeSelector();
} else {
errorWithMessage("Expected ID, CLASS, AttributeSelector, PseudoClass, or TypeSelect as negation argument");
}
break;
}
return result;
}
private void lexerDidPopSource() {
if (activeImports.size() > 0) {
activeImports.pop();
} else {
PXLog.e(TAG, "Tried to pop an empty activeImports array");
}
}
/*
* Overrides the super implementation. (non-Javadoc)
* @see com.pixate.freestyle.parsing.PXParserBase#advance()
*/
@Override
public Lexeme<PXStylesheetTokenType> advance() {
Lexeme<PXStylesheetTokenType> candidate = lexer.nextLexeme();
while (candidate == null && lexerStack != null && !lexerStack.isEmpty()) {
// pop lexer
lexer = lexerStack.pop();
// notify the parser that we've done so
lexerDidPopSource();
// try getting the next lexeme from the newly activated lexer
candidate = lexer.nextLexeme();
}
return currentLexeme = candidate;
}
// Helpers
private void addImportName(String name) {
if (!StringUtil.isEmpty(name)) {
if (activeImports == null) {
activeImports = new Stack<String>();
}
activeImports.push(name);
}
}
private void advanceToType(PXStylesheetTokenType type) {
while (currentLexeme != null && currentLexeme.getType() != type
&& currentLexeme.getType() != PXStylesheetTokenType.EOF) {
advance();
}
}
private void pushLexeme(Lexeme<PXStylesheetTokenType> lexeme) {
lexer.pushLexeme((PXStylesheetLexeme) currentLexeme);
currentLexeme = lexeme;
}
private void pushSource(String source) {
if (lexerStack == null) {
lexerStack = new Stack<PXStylesheetLexer>();
}
// push current lexer
lexerStack.push(lexer);
// create new lexer and activate it
lexer = new PXStylesheetLexer();
lexer.setSource(source);
}
private String getCurrentFilename() {
return (activeImports != null && activeImports.size() > 0) ? (new File(
activeImports.lastElement())).getName() : null;
}
@Override
public void addError(String error, String filename, String offset) {
offset = (currentLexeme.getType() != PXStylesheetTokenType.EOF) ? String
.valueOf(currentLexeme.getOffset()) : "EOF";
super.addError(error, filename, offset);
}
}