/*
* 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.
*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*/
package com.osbcp.cssparser;
import java.util.ArrayList;
import java.util.List;
import com.osbcp.cssparser.IncorrectFormatException.ErrorCode;
/**
* Main logic for the CSS parser.
*
* @author <a href="mailto:christoffer@christoffer.me">Christoffer Pettersson</a>
*/
public final class CSSParser {
/**
* Reads CSS as a String and returns back a list of Rules.
*
* @param css A String representation of CSS.
* @return A list of Rules
* @throws Exception If any errors occur.
*/
public static List<Rule> parse(final String css) throws Exception {
CSSParser parser = new CSSParser();
List<Rule> rules = new ArrayList<Rule>();
if (css == null || css.trim().isEmpty()) {
return rules;
}
for (int i = 0; i < css.length(); i++) {
char c = css.charAt(i);
if (i < css.length() - 1) {
char nextC = css.charAt(i + 1);
parser.parse(rules, c, nextC);
} else {
parser.parse(rules, c, null);
}
}
return rules;
}
public static Rule parseStyle(String style) throws Exception {
String css = "selector {"+style+"}";
List<Rule> rules = parse(css);
if(rules.size()>=1) {
Rule rule = rules.get(0);
rule.clearSelectors();
return rule;
} else {
return new Rule();
}
}
private final List<String> selectorNames;
private String selectorName;
private String propertyName;
private String valueName;
// private Map<String, String> map;
private final List<PropertyValue> values;
private State state;
private Character previousChar;
private State beforeCommentMode;
/**
* Creates a new parser.
*/
private CSSParser() {
this.selectorName = "";
this.propertyName = "";
this.valueName = "";
// this.map = new LinkedHashMap<String, String>();
this.values = new ArrayList<PropertyValue>();
this.state = State.INSIDE_SELECTOR;
this.previousChar = null;
this.beforeCommentMode = null;
this.selectorNames = new ArrayList<String>();
}
/**
* Main parse logic.
*
* @param rules The list of rules.
* @param c The current currency.
* @param nextC The next currency (or null).
* @throws Exception If any errors occurs.
*/
private void parse(final List<Rule> rules, final Character c, final Character nextC) throws Exception {
// Special case if we find a comment
if (Chars.SLASH.equals(c) && Chars.STAR.equals(nextC)) {
beforeCommentMode = state;
state = State.INSIDE_COMMENT;
}
switch (state) {
case INSIDE_SELECTOR: {
parseSelector(c);
break;
}
case INSIDE_COMMENT: {
parseComment(c);
break;
}
case INSIDE_PROPERTY_NAME: {
parsePropertyName(rules, c);
break;
}
case INSIDE_VALUE: {
parseValue(c);
break;
}
case INSIDE_VALUE_ROUND_BRACKET: {
parseValueInsideRoundBrackets(c);
break;
}
}
// Save the previous character
previousChar = c;
}
/**
* Parse a value.
*
* @param c The current character.
* @throws IncorrectFormatException If any errors occur.
*/
private void parseValue(final Character c) throws IncorrectFormatException {
// Special case if the value is a data uri, the value can contain a ;
// boolean valueHasDataURI = valueName.toLowerCase().indexOf("data:") != -1;
if (Chars.SEMI_COLON.equals(c)) {
// Store it in the values map
PropertyValue pv = new PropertyValue(propertyName.trim(), valueName.trim());
values.add(pv);
// map.put(propertyName.trim(), valueName.trim());
propertyName = "";
valueName = "";
state = State.INSIDE_PROPERTY_NAME;
return;
} else if (Chars.ROUND_BRACKET_BEG.equals(c)) {
valueName += Chars.ROUND_BRACKET_BEG;
state = State.INSIDE_VALUE_ROUND_BRACKET;
return;
} else if (Chars.COLON.equals(c)) {
throw new IncorrectFormatException(ErrorCode.FOUND_COLON_WHILE_READING_VALUE, "The value '" + valueName.trim() + "' for property '" + propertyName.trim() + "' in the selector '" + selectorName.trim() + "' had a ':' character.");
} else if (Chars.BRACKET_END.equals(c)) {
throw new IncorrectFormatException(ErrorCode.FOUND_END_BRACKET_BEFORE_SEMICOLON, "The value '" + valueName.trim() + "' for property '" + propertyName.trim() + "' in the selector '" + selectorName.trim() + "' should end with an ';', not with '}'.");
} else {
valueName += c;
return;
}
}
/**
* Parse value inside a round bracket (
*
* @param c The current character.
* @throws IncorrectFormatException If any error occurs.
*/
private void parseValueInsideRoundBrackets(final Character c) throws IncorrectFormatException {
if (Chars.ROUND_BRACKET_END.equals(c)) {
valueName += Chars.ROUND_BRACKET_END;
state = State.INSIDE_VALUE;
return;
} else {
valueName += c;
return;
}
}
/**
* Parse property name.
*
* @param rules The list of rules.
* @param c The current character.
* @throws IncorrectFormatException If any error occurs
*/
private void parsePropertyName(final List<Rule> rules, final Character c) throws IncorrectFormatException {
if (Chars.COLON.equals(c)) {
state = State.INSIDE_VALUE;
return;
} else if (Chars.SEMI_COLON.equals(c)) {
throw new IncorrectFormatException(ErrorCode.FOUND_SEMICOLON_WHEN_READING_PROPERTY_NAME, "Unexpected character '" + c + "' for property '" + propertyName.trim() + "' in the selector '" + selectorName.trim() + "' should end with an ';', not with '}'.");
} else if (Chars.BRACKET_END.equals(c)) {
Rule rule = new Rule();
/*
* Huge logic to create a new rule
*/
for (String s : selectorNames) {
Selector selector = new Selector(s.trim());
rule.addSelector(selector);
}
selectorNames.clear();
Selector selector = new Selector(selectorName.trim());
selectorName = "";
rule.addSelector(selector);
// Add the property values
for (PropertyValue pv : values) {
rule.addPropertyValue(pv);
}
// for (Entry<String, String> entry : map.entrySet()) {
//
// String property = entry.getKey();
// String value = entry.getValue();
//
// PropertyValue propertyValue = new PropertyValue(property, value);
// rule.addPropertyValue(propertyValue);
//
// }
// map.clear();
values.clear();
if (!rule.getPropertyValues().isEmpty()) {
rules.add(rule);
}
state = State.INSIDE_SELECTOR;
} else {
propertyName += c;
return;
}
}
/**
* Parse a selector.
*
* @param c The current character.
*/
private void parseComment(final Character c) {
if (Chars.STAR.equals(previousChar) && Chars.SLASH.equals(c)) {
state = beforeCommentMode;
return;
}
}
/**
* Parse a selector.
*
* @param c The current character.
* @throws IncorrectFormatException If an error occurs.
*/
private void parseSelector(final Character c) throws IncorrectFormatException {
if (Chars.BRACKET_BEG.equals(c)) {
state = State.INSIDE_PROPERTY_NAME;
return;
} else if (Chars.COMMA.equals(c)) {
if (selectorName.trim().isEmpty()) {
throw new IncorrectFormatException(ErrorCode.FOUND_COLON_WHEN_READING_SELECTOR_NAME, "Found an ',' in a selector name without any actual name before it.");
}
selectorNames.add(selectorName.trim());
selectorName = "";
} else {
selectorName += c;
return;
}
}
}