/**
* Copyright (c) 2009-2015, Christer Sandberg
*/
package se.fishtank.css.selectors.parser;
import se.fishtank.css.selectors.tokenizer.Token;
import se.fishtank.css.selectors.tokenizer.TokenType;
import se.fishtank.css.selectors.tokenizer.Tokenizer;
import se.fishtank.css.selectors.util.Pair;
/**
* Parses {@code An+B} notation.
*
* @author Christer Sandberg
*/
public class NthParser {
/** The tokenizer used when parsing. */
private final Tokenizer tokenizer;
/** Exception thrown on parsing errors. */
private final ParserException error;
/**
* Create a new parser.
*
* @param tokenizer The tokenizer to use when parsing.
*/
private NthParser(Tokenizer tokenizer) {
this.tokenizer = tokenizer;
this.error = new ParserException("Invalid nth arguments at position " + tokenizer.getPosition());
}
/**
* Parses {@code An+B} notation at the current position in the given tokenizer.
*
* @param tokenizer The tokenizer to use when parsing.
* @return The values for <i>A</i> and <i>B</i>.
*/
public static Pair<Integer, Integer> parse(Tokenizer tokenizer) {
NthParser parser = new NthParser(tokenizer);
try {
String str;
Token token = parser.skipWhitespace();
switch (token.type) {
case NUMBER:
Token.Number n = (Token.Number) token;
if (!n.integer || !parser.matchClosingParen()) {
throw parser.error;
}
return new Pair<>(0, Integer.parseInt(n.value));
case DIMENSION:
Token.Dimension d = (Token.Dimension) token;
if (!d.integer) {
throw parser.error;
}
int a = Integer.parseInt(d.value);
str = d.unit.toLowerCase();
switch (str) {
case "n":
return new Pair<>(a, parser.parseB());
case "n-":
return new Pair<>(a, parser.parseSignlessB(-1));
}
try {
return new Pair<>(a, parser.parseNDashDigits(str));
} finally {
parser.mustMatchClosingParen();
}
case IDENT:
str = token.value.toLowerCase();
switch (str) {
case "even":
parser.mustMatchClosingParen();
return new Pair<>(2, 0);
case "odd":
parser.mustMatchClosingParen();
return new Pair<>(2, 1);
case "n":
return new Pair<>(1, parser.parseB());
case "-n":
return new Pair<>(-1, parser.parseB());
case "n-":
return new Pair<>(1, parser.parseSignlessB(-1));
case "-n-":
return new Pair<>(-1, parser.parseSignlessB(-1));
}
try {
if (str.startsWith("-")) {
return new Pair<>(-1, parser.parseNDashDigits(str.substring(1)));
} else {
return new Pair<>(1, parser.parseNDashDigits(str));
}
} finally {
parser.mustMatchClosingParen();
}
case DELIM:
if (!"+".equals(token.value)) {
throw parser.error;
}
token = tokenizer.nextToken();
if (token.type != TokenType.IDENT) {
throw parser.error;
}
str = token.value.toLowerCase();
switch (str) {
case "n":
return new Pair<>(1, parser.parseB());
case "n-":
return new Pair<>(1, parser.parseSignlessB(-1));
}
try {
return new Pair<>(1, parser.parseNDashDigits(str));
} finally {
parser.mustMatchClosingParen();
}
default:
throw parser.error;
}
} catch (NumberFormatException e) {
throw parser.error;
}
}
/**
* Parse a <i>B</i> value.
*
* @return The number parsed.
*/
private int parseB() {
Token token = skipWhitespace();
switch (token.type) {
case RIGHT_PAREN:
return 0;
case DELIM:
switch (token.value) {
case "+":
return parseSignlessB(1);
case "-":
return parseSignlessB(-1);
}
break;
case NUMBER:
Token.Number n = (Token.Number) token;
if (n.integer && hasSignPrefix(n.value) && matchClosingParen()) {
return Integer.parseInt(n.value);
}
break;
}
throw error;
}
/**
* Parse a <i>B</i> value returning {@code B * sign}
*
* @param sign The sign.
* @return The number parsed.
*/
private int parseSignlessB(int sign) {
Token token = skipWhitespace();
if (token instanceof Token.Number) {
Token.Number n = (Token.Number) token;
if (n.integer && !hasSignPrefix(n.value) && matchClosingParen()) {
return Integer.parseInt(n.value) * sign;
}
}
throw error;
}
/**
* Parses the digit(s) that is prefixed by {@code n-}
*
* @param str The string to parse digits from.
* @return The number parsed.
*/
private int parseNDashDigits(String str) {
if (str.length() >= 3 && str.startsWith("n-")) {
return Integer.parseInt(str.substring(1));
}
throw error;
}
/**
* Returns whether the given string starts with {@code +} or {@code -}
*
* @param str The string to check.
* @return {@code true} or {@code false}
*/
private boolean hasSignPrefix(String str) {
char c = str.charAt(0);
return c == '+' || c == '-';
}
/**
* Returns whether the next non-whitespace token is a right parent.
*
* @return {@code true} or {@code false}
*/
private boolean matchClosingParen() {
return skipWhitespace().type == TokenType.RIGHT_PAREN;
}
/**
* Throws a parser error exception if the next non-whitespace token isn't a right paren.
*/
private void mustMatchClosingParen() {
if (!matchClosingParen()) {
throw error;
}
}
/**
* Returns the next non-whitespace token from the given tokenizer.
*
* @return The next non-whitespace token.
*/
private Token skipWhitespace() {
while (true) {
Token token = tokenizer.nextToken();
if (token.type != TokenType.WHITESPACE) {
return token;
}
}
}
}