package com.mozz.htmlnative.parser;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.mozz.htmlnative.HNLog;
import com.mozz.htmlnative.HNSegment;
import com.mozz.htmlnative.css.StyleSheet;
import com.mozz.htmlnative.css.selector.AnySelector;
import com.mozz.htmlnative.css.selector.ClassSelector;
import com.mozz.htmlnative.css.selector.CssSelector;
import com.mozz.htmlnative.css.selector.IdSelector;
import com.mozz.htmlnative.css.selector.TypeSelector;
import com.mozz.htmlnative.exception.HNSyntaxError;
import com.mozz.htmlnative.parser.token.Token;
import com.mozz.htmlnative.parser.token.TokenType;
import java.io.EOFException;
import java.util.Map;
import static com.mozz.htmlnative.parser.StyleItemParser.parseKey;
import static com.mozz.htmlnative.parser.StyleItemParser.parseStyleSingle;
/**
* @author Yang Tao, 17/3/26.
*/
public final class CssParser {
private static final int SELECTOR_HASH = 1 << 7;
private static final int SELECTOR_DOT = 1 << 8;
private static final int SELECTOR_ID = 1;
private static final int SELECTOR_STAR = 1 << 16;
private static final int SELECTOR_CLASS = 1 << 1;
private static final int SELECTOR_TYPE = 1 << 2;
private static final int START_BRACE = 1 << 3;
private static final int END_BRACE = 1 << 4;
private static final int KEY = 1 << 5;
private static final int VALUE_STRING = 1 << 6;
private static final int VALUE_INT = 1 << 9;
private static final int VALUE_DOUBLE = 1 << 10;
private static final int VALUE_HASH = 1 << 11;
private static final int VALUE_START_PAREN = 1 << 12;
private static final int VALUE_END_PAREN = 1 << 13;
private static final int COLON = 1 << 7;
private static final int SEMICOLON = 1 << 8;
private static final int COMMA = 1 << 14;
private static final int END_ANGLE_BRACKET = 1 << 15;
private static final int SELECTOR_START = SELECTOR_HASH | SELECTOR_TYPE | SELECTOR_DOT |
SELECTOR_STAR;
private static final int VALUE = VALUE_STRING | VALUE_INT | VALUE_DOUBLE | VALUE_HASH |
VALUE_START_PAREN | VALUE_END_PAREN;
private int lookFor;
private static final int CHAIN_DESCENDANT = 0x01;
private static final int CHAIN_CHILD = 0x02;
private static final int CHAIN_GROUP = 0x03;
@Nullable
private Token mCurToken;
private final CssLexer lexer;
private Map<String, Object> styleCache;
public CssParser(Lexer lexer, Parser parentParser) {
this.lexer = new CssLexer(lexer);
this.styleCache = parentParser.getStyleCache();
}
/**
* Static Method to parse inline style into {@code Map<String, Object>}
*
* @param styleString, inline style string to parse
* @param bufferToUse, buffer to use, may overwrite its content
* @param out, out parameter, to store the result of parsed data.
*/
public static void parseInlineStyle(@NonNull String styleString, StringBuilder bufferToUse,
Map<String, Object> out) {
bufferToUse.setLength(0);
String key = null;
out.clear();
boolean inBracket = false;
for (int i = 0; i < styleString.length(); i++) {
char c = styleString.charAt(i);
if (c == '(') {
inBracket = true;
bufferToUse.append(c);
} else if (c == ')') {
inBracket = false;
bufferToUse.append(c);
} else if (c == ';') {
Object value = out.get(parseKey(key));
StyleHolder parsedStyle;
if (value != null) {
parsedStyle = parseStyleSingle(key, bufferToUse.toString(), value);
} else {
parsedStyle = parseStyleSingle(key, bufferToUse.toString(), null);
}
out.put(parsedStyle.key, parsedStyle.obj);
bufferToUse.setLength(0);
} else if (c == ':' && !inBracket) {
key = bufferToUse.toString();
bufferToUse.setLength(0);
} else {
if (c == ' ' || c == '\r' || c == '\n' || c == '\t' || c == '\f' || c == '\b') {
continue;
}
bufferToUse.append(c);
}
}
if (key != null) {
Object value = out.get(parseKey(key));
StyleHolder parsedStyle;
if (value != null) {
parsedStyle = parseStyleSingle(key, bufferToUse.toString(), value);
} else {
parsedStyle = parseStyleSingle(key, bufferToUse.toString(), null);
}
out.put(parsedStyle.key, parsedStyle.obj);
}
bufferToUse.setLength(0);
}
void process(HNSegment segment) throws EOFException, HNSyntaxError {
StyleSheet styleSheet = segment.getStyleSheet();
lookFor(SELECTOR_START);
CssSelector cssSelector = null;
String keyCache = null;
int chainType = CHAIN_DESCENDANT;
while (true) {
scan();
switch (mCurToken.type()) {
case Comma:
check(COMMA);
lookFor(SELECTOR_START);
chainType = CHAIN_GROUP;
break;
case EndAngleBracket:
check(END_ANGLE_BRACKET);
lookFor(SELECTOR_START);
chainType = CHAIN_CHILD;
break;
case Hash:
check(SELECTOR_HASH | VALUE);
lookFor(SELECTOR_ID);
break;
case Id:
String idValue = mCurToken.stringValue();
// tag selector should be in the first position of whole if-statement
if (isLookingFor(SELECTOR_TYPE)) {
if (cssSelector == null) {
cssSelector = new TypeSelector(idValue);
styleSheet.register(cssSelector);
} else {
CssSelector groupOne = new TypeSelector(idValue);
if (chain(cssSelector, groupOne, chainType)) {
styleSheet.putSelector(cssSelector);
cssSelector = groupOne;
}
}
lookFor(START_BRACE | SELECTOR_START | COMMA | END_ANGLE_BRACKET);
chainType = CHAIN_DESCENDANT;
} else if (isLookingFor(SELECTOR_CLASS)) {
if (cssSelector == null) {
cssSelector = new ClassSelector(idValue);
styleSheet.register(cssSelector);
} else {
CssSelector groupOne = new ClassSelector(idValue);
if (chain(cssSelector, groupOne, chainType)) {
styleSheet.putSelector(cssSelector);
cssSelector = groupOne;
}
}
lookFor(START_BRACE | SELECTOR_START | COMMA | END_ANGLE_BRACKET);
chainType = CHAIN_DESCENDANT;
} else if (isLookingFor(SELECTOR_ID)) {
if (cssSelector == null) {
cssSelector = new IdSelector(idValue);
styleSheet.register(cssSelector);
} else {
CssSelector groupOne = new IdSelector(idValue);
if (chain(cssSelector, groupOne, chainType)) {
styleSheet.putSelector(cssSelector);
cssSelector = groupOne;
}
}
lookFor(START_BRACE | SELECTOR_START | COMMA | END_ANGLE_BRACKET);
chainType = CHAIN_DESCENDANT;
} else if (isLookingFor(KEY)) {
check(KEY);
keyCache = idValue;
lookFor(COLON);
}
break;
case Dot:
check(SELECTOR_DOT);
lookFor(SELECTOR_CLASS);
break;
case Star:
check(SELECTOR_STAR);
lookFor(START_BRACE | SELECTOR_START | COMMA | END_ANGLE_BRACKET);
if (cssSelector == null) {
cssSelector = new AnySelector();
styleSheet.register(cssSelector);
} else {
CssSelector groupOne = new AnySelector();
if (chain(cssSelector, groupOne, chainType)) {
styleSheet.putSelector(cssSelector);
cssSelector = groupOne;
}
}
chainType = CHAIN_DESCENDANT;
break;
case Colon:
check(COLON);
lookFor(VALUE);
shouldScanValue = true;
break;
case StartBrace:
check(START_BRACE);
lookFor(KEY | END_BRACE);
break;
case EndBrace:
check(END_BRACE);
lookFor(SELECTOR_START);
styleSheet.putSelector(cssSelector);
// put all the attr in styleSheet
for (Map.Entry<String, Object> entry : styleCache.entrySet()) {
styleSheet.put(cssSelector, entry.getKey(), entry.getValue());
}
styleCache.clear();
cssSelector = null;
break;
case Value:
check(VALUE);
Object value = styleCache.get(parseKey(keyCache));
StyleHolder parsedStyle;
if (value != null) {
parsedStyle = parseStyleSingle(keyCache, mCurToken.stringValue(), value);
} else {
parsedStyle = parseStyleSingle(keyCache, mCurToken.stringValue(), null);
}
styleCache.put(parsedStyle.key, parsedStyle.obj);
lookFor(VALUE | END_BRACE | SEMICOLON);
break;
case Semicolon:
check(SEMICOLON);
lookFor(END_BRACE | KEY);
break;
// Below is special case, to handle the class or id selector which have the same
// name with Head, Meta, Script, Template, Body, Link, Style, Html and Title. The
// process is the same with Id token.
case Head:
case Meta:
case Script:
case Template:
case Body:
case Link:
case Style:
case Html:
case Title:
check(SELECTOR_CLASS | SELECTOR_ID | SELECTOR_TYPE);
if (isLookingFor(SELECTOR_CLASS)) {
if (cssSelector == null) {
cssSelector = new ClassSelector(mCurToken.stringValue());
styleSheet.register(cssSelector);
} else {
CssSelector groupOne = new ClassSelector(mCurToken.stringValue());
if (chain(cssSelector, groupOne, chainType)) {
styleSheet.putSelector(cssSelector);
cssSelector = groupOne;
}
}
} else if (isLookingFor(SELECTOR_ID)) {
if (cssSelector == null) {
cssSelector = new IdSelector(mCurToken.stringValue());
styleSheet.register(cssSelector);
} else {
CssSelector groupOne = new IdSelector(mCurToken.stringValue());
if (chain(cssSelector, groupOne, chainType)) {
styleSheet.putSelector(cssSelector);
cssSelector = groupOne;
}
}
} else if (isLookingFor(SELECTOR_TYPE)) {
if (cssSelector == null) {
cssSelector = new TypeSelector(mCurToken.stringValue());
styleSheet.register(cssSelector);
} else {
CssSelector groupOne = new TypeSelector(mCurToken.stringValue());
if (chain(cssSelector, groupOne, chainType)) {
styleSheet.putSelector(cssSelector);
cssSelector = groupOne;
}
}
}
lookFor(START_BRACE | SELECTOR_START);
break;
case StartAngleBracket:
check(SELECTOR_START);
scan();
if (mCurToken.type() == TokenType.Slash) {
HNLog.d(HNLog.CSS_PARSER, styleSheet.toString());
return;
}
// if parse process didn't end, then there is a syntax error.
default:
HNLog.e(HNLog.CSS_PARSER, "unknown token " + mCurToken.toString() + " when " +
"parsing css");
throw new HNSyntaxError("unknown token " + mCurToken.toString() + " when " +
"parsing css", lexer.line(), lexer.column());
}
}
}
private boolean isLookingFor(int status) {
return (lookFor & status) != 0;
}
private void lookFor(int status) {
lookFor = 0;
lookFor |= status;
}
private void scan() throws EOFException, HNSyntaxError {
if (mCurToken != null) {
mCurToken.recycle();
}
mCurToken = lexer.scan();
HNLog.d(HNLog.CSS_PARSER, "StyleSheet -> next is " + mCurToken.toString());
}
private boolean shouldScanValue = false;
private class CssLexer {
private Lexer lexer;
private StringBuilder buffer = new StringBuilder();
CssLexer(Lexer lexer) {
super();
this.lexer = lexer;
}
@Nullable
public Token scan() throws EOFException, HNSyntaxError {
lexer.skipWhiteSpace();
if (shouldScanValue) {
return scanValue();
} else if (peek() == '-') {
// hook the - case, to handle the style name such as -webkit-**.
return scanIdWithMinus();
} else {
return lexer.scan();
}
}
public char peek() {
return lexer.peek();
}
Token scanValue() throws EOFException {
long startColumn = lexer.column();
long line = lexer.line();
lexer.skipWhiteSpace();
buffer.setLength(0);
if (peek() == ';') {
lexer.next();
return Token.obtainToken(TokenType.Value, "", line, startColumn);
}
do {
buffer.append(peek());
lexer.next();
if (peek() == ';' || peek() == '}') {
break;
}
} while (true);
shouldScanValue = false;
return Token.obtainToken(TokenType.Value, buffer.toString(), line, startColumn);
}
Token scanIdWithMinus() throws EOFException {
long startColumn = lexer.column();
long line = lexer.line();
buffer.setLength(0);
do {
buffer.append(peek());
lexer.next();
}
while (Lexer.isLetter(peek()) || Lexer.isDigit(peek()) || peek() == '.' || peek() ==
'-' || peek() == '_');
String idStr = buffer.toString();
TokenType type = TokenType.Id;
return Token.obtainToken(type, buffer.toString(), line, startColumn);
}
public long line() {
return lexer.line();
}
public long column() {
return lexer.column();
}
}
private void check(int status) throws HNSyntaxError {
if (!isLookingFor(status)) {
HNLog.d(HNLog.CSS_PARSER, " Looking for " + lookForToString(status) + ", but " +
"currently is " +
lookForToString(this.lookFor));
throw new HNSyntaxError(" Looking for " + lookForToString(status) + ", but " +
"currently is " +
lookForToString(this.lookFor), lexer.line(), lexer.column());
}
}
/**
* @param root
* @param newCss
* @param chainType
* @return switch Root With NewCss if true
*/
private static boolean chain(CssSelector root, CssSelector newCss, int chainType) {
switch (chainType) {
case CHAIN_CHILD:
root.chainChild(newCss, false);
return false;
case CHAIN_GROUP:
// if chain group happens, should return true, than root will become the newCss
root.chainGroup(newCss);
return true;
case CHAIN_DESCENDANT:
root.chainChild(newCss, true);
return false;
default:
throw new IllegalStateException();
}
}
private static String lookForToString(int lookFor) {
StringBuilder sb = new StringBuilder("[ ");
if ((lookFor & SELECTOR_HASH) != 0) {
sb.append("# ");
}
if ((lookFor & SELECTOR_DOT) != 0) {
sb.append(". ");
}
if ((lookFor & SELECTOR_ID) != 0) {
sb.append("id ");
}
if ((lookFor & SELECTOR_CLASS) != 0) {
sb.append("class ");
}
if ((lookFor & SELECTOR_TYPE) != 0) {
sb.append("type ");
}
if ((lookFor & START_BRACE) != 0) {
sb.append("{ ");
}
if ((lookFor & END_BRACE) != 0) {
sb.append("} ");
}
if ((lookFor & KEY) != 0) {
sb.append("cssPropertyName ");
}
if ((lookFor & VALUE) != 0) {
sb.append("cssPropertyValue ");
}
if ((lookFor & COLON) != 0) {
sb.append(": ");
}
if ((lookFor & SEMICOLON) != 0) {
sb.append("; ");
}
sb.append(" ]");
return sb.toString();
}
public static class StyleHolder {
public String key;
public Object obj;
public String cacheKey;
}
}