package org.angularjs.lang.parser;
import com.intellij.lang.PsiBuilder;
import com.intellij.lang.javascript.JSElementTypes;
import com.intellij.lang.javascript.JSTokenTypes;
import com.intellij.lang.javascript.parsing.ExpressionParser;
import com.intellij.psi.tree.IElementType;
import org.angularjs.codeInsight.AngularJSPluralCategories;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* @author Irina.Chernushina on 11/30/2015.
*/
public class AngularJSMessageFormatParser extends ExpressionParser<AngularJSParser> {
@NonNls public static final String OFFSET_OPTION = "offset";
private boolean myInsideSelectExpression = false;
public AngularJSMessageFormatParser(@NotNull AngularJSParser parser) {
super(parser);
}
public boolean parseMessage() {
if (myInsideSelectExpression) return false;
final PsiBuilder.Marker expr = builder.mark();
if (! isIdentifierToken(builder.getTokenType())) {
return rollback(expr);
}
final PsiBuilder.Marker refMark = builder.mark();
//if (!myJavaScriptParser.getExpressionParser().parseQualifiedTypeName()) {
myInsideSelectExpression = true;
try {
if (!myJavaScriptParser.getExpressionParser().parseUnaryExpression()) {
return rollback(expr);
}
} finally {
myInsideSelectExpression = false;
}
//}
refMark.done(JSElementTypes.EXPRESSION_STATEMENT);
if (builder.getTokenType() != JSTokenTypes.COMMA) {
return rollback(expr);
}
builder.advanceLexer();
final String extensionText = builder.getTokenText();
if (!isKnownExtension(extensionText) || !isIdentifierToken(builder.getTokenType())) {
return rollback(expr);
}
collapseTokenElement(AngularJSElementTypes.MESSAGE_FORMAT_EXPRESSION_NAME);
if (builder.getTokenType() != JSTokenTypes.COMMA) {
return rollback(expr);
}
builder.advanceLexer();
if (ExtensionType.select.name().equals(extensionText)) {
parseOptionsTail();
} else {
parsePluralTail();
}
expr.done(AngularJSElementTypes.MESSAGE_FORMAT_EXPRESSION);
return true;
}
public boolean parseInnerMessage() {
final PsiBuilder.Marker mark = builder.mark();
PsiBuilder.Marker stringLiteralMark = null;
while (!builder.eof()) {
final IElementType type = builder.getTokenType();
if (JSTokenTypes.LBRACE == type || JSTokenTypes.RBRACE == type) {
if (stringLiteralMark != null) {
stringLiteralMark.collapse(JSTokenTypes.STRING_LITERAL);
stringLiteralMark = null;
}
if (JSTokenTypes.LBRACE == type) {
if (JSTokenTypes.LBRACE == builder.lookAhead(1)) {
builder.advanceLexer();
builder.advanceLexer();
myJavaScriptParser.getExpressionParser().parseExpression();
if (! expectDoubleRBrace(true)) {
mark.drop();
return false;
}
}
else {
builder.error("expected {{");
mark.drop();
return false;
}
}
else if (JSTokenTypes.RBRACE == type) {
mark.done(AngularJSElementTypes.MESSAGE_FORMAT_MESSAGE);
builder.advanceLexer();
return true;
}
} else {
if (stringLiteralMark == null) stringLiteralMark = builder.mark();
builder.advanceLexer();
}
}
if (stringLiteralMark != null) stringLiteralMark.drop();
mark.drop();
return false;
}
private boolean expectDoubleRBrace(boolean advance) {
if (!isRBraceOrNull(builder.getTokenType()) || !isRBraceOrNull(builder.lookAhead(1))) {
builder.error("expected }}");
return false;
}
if (advance) {
builder.advanceLexer();
builder.advanceLexer();
}
return true;
}
private static boolean isRBraceOrNull(IElementType type) {
return type == null || JSTokenTypes.RBRACE == type;
}
private void parsePluralTail() {
if (!parseOffsetOption()) return;
parseOptionsTail();
}
private boolean parseOffsetOption() {
if (isIdentifierToken(builder.getTokenType()) && OFFSET_OPTION.equals(builder.getTokenText())) {
if (builder.lookAhead(1) != JSTokenTypes.COLON) {
builder.advanceLexer();
builder.error("expected colon");
return false;
}
final IElementType value = builder.lookAhead(2);
if (!JSTokenTypes.LITERALS.contains(value) && JSTokenTypes.IDENTIFIER != value) {
builder.advanceLexer();
builder.advanceLexer();
builder.error("expected offset option value");
return false;
}
final PsiBuilder.Marker mark = builder.mark();
builder.advanceLexer();// offset
builder.advanceLexer();// colon
builder.advanceLexer();// value
mark.done(AngularJSElementTypes.MESSAGE_FORMAT_OPTION);
}
return true;
}
private void parseOptionsTail() {
boolean key = true;
while (!builder.eof()) {
final IElementType type = builder.getTokenType();
if (key) {
if (JSTokenTypes.RBRACE == type) {
expectDoubleRBrace(false);
return;
} else if (JSTokenTypes.LBRACE == type) {
builder.error("expected selection keyword");
return;
} else {
final PsiBuilder.Marker mark = builder.mark();
// = can be only in the beginning, like =0
while (!JSTokenTypes.PARSER_WHITE_SPACE_TOKENS.contains(builder.rawLookup(0)) && builder.rawLookup(0) != null) {
builder.advanceLexer();
}
mark.collapse(AngularJSElementTypes.MESSAGE_FORMAT_SELECTION_KEYWORD);
key = false;
}
} else {
if (JSTokenTypes.LBRACE == type) {
builder.advanceLexer();
if (!parseInnerMessage()) return; //+-
key = true;
} else {
builder.error("expected message in {} delimiters");
return;
}
}
}
}
private void collapseTokenElement(IElementType type) {
final PsiBuilder.Marker mark = builder.mark();
builder.advanceLexer();
mark.collapse(type);
}
private static boolean isKnownExtension(String text) {
return ExtensionType.select.name().equals(text) || ExtensionType.plural.name().equals(text);
}
private static boolean rollback(PsiBuilder.Marker expr) {
expr.rollbackTo();
return false;
}
public enum ExtensionType {
plural(AngularJSPluralCategories.other.name()), select("other");
private final Set<String> myRequiredSelectionKeywords;
ExtensionType(String... keywords) {
if (keywords.length == 0) {
myRequiredSelectionKeywords = null;
} else {
myRequiredSelectionKeywords = new HashSet<>();
Collections.addAll(myRequiredSelectionKeywords, keywords);
}
}
@NotNull
public Set<String> getRequiredSelectionKeywords() {
return myRequiredSelectionKeywords == null ? Collections.emptySet() : myRequiredSelectionKeywords;
}
}
}