All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.fizzed.rocker.compiler.TemplateParser Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2015 Fizzed 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.fizzed.rocker.compiler;

import com.fizzed.rocker.ContentType;
import com.fizzed.rocker.antlr4.RockerLexer;
import com.fizzed.rocker.antlr4.RockerParser;
import com.fizzed.rocker.antlr4.RockerParserBaseListener;
import com.fizzed.rocker.model.*;
import com.fizzed.rocker.runtime.ParserException;
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.misc.Interval;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Stack;

import static com.fizzed.rocker.compiler.RockerUtil.isJava8Plus;

/**
 *
 * @author joelauer
 */
public class TemplateParser {
    static private final Logger log = LoggerFactory.getLogger(TemplateParser.class);
    
    final private RockerConfiguration configuration;
    
    public TemplateParser(RockerConfiguration configuration) {
        this.configuration = configuration;
    }

    public RockerConfiguration getConfiguration() {
        return configuration;
    }
   
    static class TemplateIdentity {
        public File templateFile;           // e.g. src/main/java/views/index.rocker.html
        public String packageName;          // e.g. views
        public String templateName;         // e.g. index.rocker.html
        public String name;                 // e.g. index
        public ContentType contentType;     // e.g. HTML
    }
    
    static TemplateIdentity parseIdentity(File baseDirectory, File templateFile) throws IOException {
        TemplateIdentity identity = new TemplateIdentity();
        
        identity.templateFile = templateFile;
        
        // deduce "package" of file by relativizing it to input directory
        identity.packageName = RockerUtil.pathToPackageName(templateFile.toPath());
        
        Path p = templateFile.getAbsoluteFile().toPath().normalize();
        
        if (baseDirectory != null) {
            Path bdp = baseDirectory.getAbsoluteFile().toPath().normalize();
            
            if (!RockerUtil.isRelativePath(bdp, p)) {
                throw new IOException("Template file [" + templateFile + "] not relative to base dir [" + baseDirectory + "]");
            }
            
            Path relativePath = bdp.relativize(p);
            
            // new package name is the parent of this file
            identity.packageName = RockerUtil.pathToPackageName(relativePath.getParent());
        }
        
        identity.templateName = templateFile.getName();
        identity.name = RockerUtil.templateNameToName(identity.templateName);
        identity.contentType = RockerUtil.templateNameToContentType(identity.templateName);
        
        return identity;
    }
    
    public TemplateModel parse(File f) throws IOException, ParserException {
        if (!f.exists() || !f.canRead()) {
            throw new IOException("File cannot read or does not exist [" + f + "]");
        }
        
        TemplateIdentity identity = parseIdentity(this.configuration.getTemplateDirectory(), f);
        
        ANTLRFileStream input = new ANTLRFileStream(f.getPath(), "UTF-8");
        
        return parse(input, identity.packageName, identity.templateName, f.lastModified());
    }
    
    public TemplateModel parse(File f, String packageName) throws IOException, ParserException {
        ANTLRFileStream input = new ANTLRFileStream(f.getPath(), "UTF-8");
        return parse(input, packageName, f.getName(), f.lastModified());
    }
    
    public TemplateModel parse(String source, String qualifiedName) throws IOException, ParserException {
        ANTLRInputStream input = new ANTLRInputStream(source);
        input.name = qualifiedName;
        return parse(input, "views", qualifiedName, -1);
    }
    
    static public ParserException buildParserException(SourceRef sourceRef, String templatePath, String msg) {
        return new ParserException(sourceRef.getBegin().getLineNumber(), sourceRef.getBegin().getPosInLine(), templatePath, msg, null);
    }
    
    static public ParserException buildParserException(SourceRef sourceRef, String templatePath, String msg, Throwable cause) {
        return new ParserException(sourceRef.getBegin().getLineNumber(), sourceRef.getBegin().getPosInLine(), templatePath, msg, cause);
    }
    
    private TemplateModel parse(ANTLRInputStream input, String packageName, String templateName, long modifiedAt) throws ParserException {
        // construct path for more helpful error messages
        String templatePath = packageName.replace(".", File.separator) + "/" + templateName;
        
        // get our lexer
        log.trace("Lexer for input stream");
        RockerLexer lexer = new RockerLexer(input);
        lexer.removeErrorListeners();
        lexer.addErrorListener(new DescriptiveErrorListener());

        //
        // lexer
        //
        CommonTokenStream tokens = null;
        try {
            // get a list of matched tokens
            log.trace("Tokenizing lexer");
            tokens = new CommonTokenStream(lexer);
        } catch (ParserRuntimeException e) {
            throw unwrapParserRuntimeException(templatePath, e);
        }
        
        if (log.isTraceEnabled()) {
            // just for debugging lexer
            tokens.fill();
            for (Token token : tokens.getTokens()) {
                String tokenName = null;
                final Integer type = token.getType();
                for (Map.Entry values : lexer.getTokenTypeMap().entrySet()) {
                    if (Objects.equals(values.getValue(), type)) {
                        tokenName = values.getKey();
                    }
                }

                log.trace("{}: {}", tokenName, token);
            }
        }
        
        //
        // parser & new model
        //
        try {
            // pass the tokens to the parser
            log.trace("Parsing tokens");
            RockerParser parser = new RockerParser(tokens);
            parser.removeErrorListeners();
            parser.addErrorListener(new DescriptiveErrorListener());
            
            TemplateModel model = new TemplateModel(packageName, templateName, modifiedAt, configuration.getOptions().copy());
            
            // walk it and attach our listener
            TemplateParserListener listener = new TemplateParserListener(input, model, templatePath);
            ParseTreeWalker walker = new ParseTreeWalker();
            log.trace("Walking parse tree");
            walker.walk(listener, parser.template());
            
            if (model.getOptions().getCombineAdjacentPlain()) {
                combineAdjacentPlain(model);
            }

            // discard whitespace either globally or template-set or also fallsback
            // to the default per content-type
            if (model.getOptions().getDiscardLogicWhitespaceForContentType(model.getContentType())) {
                discardLogicWhitespace(model);
            }
            
            return model;
        } 
        catch (ParserRuntimeException e) {
            throw unwrapParserRuntimeException(templatePath, e);
        }
    }
    
    public static ParserException unwrapParserRuntimeException(String templatePath, ParserRuntimeException e) {
        return new ParserException(e.getLine(), e.getPosInLine(), templatePath, e.getMessage(), e.getCause());
    }
    
    public void combineAdjacentPlain(TemplateModel model) throws ParserRuntimeException {
        PlainText lastPlainText = null;
        
        for (int i = 0; i < model.getUnits().size(); ) {
            TemplateUnit unit = model.getUnits().get(i);
            
            if (unit instanceof PlainText) {
   
                PlainText plainText = (PlainText)unit;
                
                if (lastPlainText != null) {
                    
                    try {
                        log.trace("Combining plainText @ {} with {}", lastPlainText.getSourceRef(), plainText.getSourceRef());
                        
                        // combine with last and create new "last"
                        lastPlainText = lastPlainText.combineAdjacent(plainText);
                    }
                    catch (TokenException e) {
                        throw new ParserRuntimeException(plainText.getSourceRef(), e.getMessage(), e);
                    }
                    
                    // replace last unit with the combined version
                    model.getUnits().set(i - 1, lastPlainText);
                    
                    // remove current node
                    model.getUnits().remove(i);
                    
                    // move onto next w/o swapping in last value
                    continue;
                }
                
                lastPlainText = plainText;
            }
            else {
                lastPlainText = null;
            }
            
            i++;
        }
    }
    
    public void discardLogicWhitespace(TemplateModel model) {
        // Discard any lines that are only whitespace up till the first line of non-whitespace
        // text or an expression that would output a value.
        boolean withinContentClosure = false;
        
        for (int i = 0; i < model.getUnits().size(); i++) {
            TemplateUnit unit = model.getUnits().get(i);
            
            // content closures are unique beasts - they are assignments and anything
            // inside of them doesn't count in this calculation
            if (unit instanceof ContentClosureBegin) {
                withinContentClosure = true;
            } else if (unit instanceof ContentClosureEnd) {
                withinContentClosure = false;
            }
            
            if (withinContentClosure) {
                continue;
            }
            
            if (!unit.isBlockLevel()) {
                if (unit instanceof PlainText) {
                    PlainText plain = (PlainText)unit;
                    if (plain.isWhitespace()) {
                        // trim this plain text, but keep searching for next one
                        plain.trim();
                    } else {
                        while (plain.chompLeadingWhitespaceToEndOfLine() > 0) {
                            // keep chomping until we get to the good stuff
                        }
                        // we are done
                        break;
                    }
                } else {
                    break;
                }
            }
        }
        
        // Then discard lines consisting entirely of block-level logic such as if/else
        // blocks, or content/value closures.  Lines that mix non-whitespace text and
        // block-level logic will be skipped.
        // only need to start on second node (since we look behind/forward by 1)
        for (int i = 1; i < model.getUnits().size(); i++) {
            TemplateUnit unit = model.getUnits().get(i);
            if (!unit.isBlockLevel()) {
                continue;
            }
            
            int prevUnitTrailingWhitespaceLengthToStartOfLine = -1;
            PlainText prevPlain = null;
            TemplateUnit prevUnit = model.getUnits().get(i-1);
            if (prevUnit instanceof PlainText) {
                prevPlain = (PlainText)prevUnit;
                prevUnitTrailingWhitespaceLengthToStartOfLine = 
                        prevPlain.trailingWhitespaceLengthToStartOfLine();
            }
            
            boolean lastUnit = ((i+1) == model.getUnits().size());
            
            int nextUnitLeadingWhitespaceLengthToEndOfLine = -1;
            PlainText nextPlain = null;
            if (!lastUnit) {
                TemplateUnit nextUnit = model.getUnits().get(i+1);
                if (nextUnit instanceof PlainText) {
                    nextPlain = (PlainText)nextUnit;
                    nextUnitLeadingWhitespaceLengthToEndOfLine =
                        nextPlain.leadingWhitespaceLengthToEndOfLine();
                }
            }
            
            // do we chop this line?
            if ((prevPlain != null && prevUnitTrailingWhitespaceLengthToStartOfLine >= 0) &&
                    (lastUnit || (nextPlain != null && nextUnitLeadingWhitespaceLengthToEndOfLine >= 0))) {
                
                prevPlain.chompTrailingLength(prevUnitTrailingWhitespaceLengthToStartOfLine);
                if (nextPlain != null && nextUnitLeadingWhitespaceLengthToEndOfLine > 0) {
                    // we actually want to chop the newline char as well!
                    nextPlain.chompLeadingLength(nextUnitLeadingWhitespaceLengthToEndOfLine);
                }
            }
        }
        
        // remove any empty plain text units (since many above may have been chopped down to nothing)
        for (int i = 0; i < model.getUnits().size(); ) {
            TemplateUnit unit = model.getUnits().get(i);
            if (unit instanceof PlainText) {
                PlainText plain = (PlainText)unit;
                if (plain.getText().isEmpty()) {
                    model.getUnits().remove(i);
                    continue;
                }
            }
            i++;
        }
    }
    
    static public class TemplateParserListener extends RockerParserBaseListener {
        
        private final ANTLRInputStream input;
        private final TemplateModel model;
        private final String templatePath;

        // We need to forbid an else { } for a @with block that has multiple arguments.
        // So using a stack to pop & push this so we can find the matching WithStatement
        // in the WithElse part.
        private final Stack withStatements = new Stack<>();
        
        public TemplateParserListener(ANTLRInputStream input, TemplateModel model, String templatePath) {
            this.input = input;
            this.model = model;
            this.templatePath = templatePath;
        }
        
        public SourceRef createSourceRef(ParserRuleContext rule) {
            return createSourceRef(rule, rule.getStart(), rule.getStop());
        }

        public SourceRef createSourceRef(ParserRuleContext rule, Token start, Token stop) {
            // antlr's position in line is zero-based, but humans think in 1-based
            SourcePosition begin = new SourcePosition(start.getLine(), start.getCharPositionInLine() + 1, start.getStartIndex());
            
            // stop index is inclusive -- we want it exclusive
            int stopIndex = stop.getStopIndex() + 1;
            
            int length = stopIndex - start.getStartIndex();
            
            /**
            // the stop index actually is correct but uses an inclusive value
            // and we want to think in where the next token starts as the end
            int endPosInFile = stop.getStopIndex() + 1;
            stop.getTokenSource().
            SourcePosition end = new SourcePosition(stop.getLine(), stop.getCharPositionInLine(), endPosInFile);
            */

            String text = input.getText(new Interval(begin.getPosInFile(), stop.getStopIndex()));

            return new SourceRef(begin, length, text);
        }
        
        @Override
        public void enterEveryRule(ParserRuleContext ctx) {
            if (log.isTraceEnabled()) {
                log.trace("{} entered with {}", ctx.getClass().getName(), ctx.toStringTree());
            }
        }
        
        @Override
        public void exitEveryRule(ParserRuleContext ctx) {
            if (log.isTraceEnabled()) {
                log.trace("{} exited with {}", ctx.getClass().getName(), ctx.toStringTree());
            }
        }

        public void verifyTemplateHeaderElementOK() {
            // any template units other than whitespace?
            for (TemplateUnit unit : this.model.getUnits()) {
                if (unit instanceof Comment) {
                    // okay
                } else if (unit instanceof PlainText) {
                    PlainText plain = (PlainText)unit;
                    if (plain.isWhitespace()) {
                        // okay
                    } else {
                        // no plain allowed
                        SourcePosition pos = plain.findSourcePositionOfNonWhitespace();
                        throw new ParserRuntimeException(pos.getLineNumber(), pos.getPosInLine(), "plain text not allowed before end of template header");
                    }
                }
            }
            
            // discard whitespace-only plain text
            List units = this.model.getUnits();
            for (int i = 0; i < units.size(); ) {
                TemplateUnit unit = units.get(i);
                if (unit instanceof PlainText) {
                    log.trace("Discarding whitespace-only plain text in template header");
                    units.remove(i);
                } else {
                    i++;
                }
            }
        }
        
        public boolean areWeCurrentlyInAForLoop() {
            int depth = 0;
            
            // start from where we are and search backwards
            for (int i = this.model.getUnits().size() - 1; i >= 0; i--) {
                TemplateUnit unit = this.model.getUnits().get(i);
                if (unit instanceof ForBlockBegin) {
                    if (depth == 0) {
                        return true;         // we are good!
                    } else {
                        depth--;
                    }
                } else if (unit instanceof ForBlockEnd) {
                    depth++;
                }
            }
            
            return false;
        }

        public boolean areWeCurrentlyInASwitchBlock() {
            int depth = 0;

            // start from where we are and search backwards
            for (int i = this.model.getUnits().size() - 1; i >= 0; i--) {
                TemplateUnit unit = this.model.getUnits().get(i);
                if (unit instanceof SwitchBlock) {
                    if (depth == 0) {
                        return true;         // we are good!
                    } else {
                        depth--;
                    }
                } else if (unit instanceof SwitchBlockEnd) {
                    depth++;
                }
            }

            return false;
        }

        public boolean areWeCurrentlyInACase() {
            int depth = 0;

            // start from where we are and search backwards
            for (int i = this.model.getUnits().size() - 1; i >= 0; i--) {
                TemplateUnit unit = this.model.getUnits().get(i);
                if (unit instanceof SwitchCaseBlock) {
                    if (depth == 0) {
                        return true;         // we are good!
                    } else {
                        depth--;
                    }
                } else if (unit instanceof SwitchCaseBlockEnd) {
                    depth++;
                }
            }

            return false;
        }

        public boolean areWeCurrentlyInADefault() {
            int depth = 0;

            // start from where we are and search backwards
            for (int i = this.model.getUnits().size() - 1; i >= 0; i--) {
                TemplateUnit unit = this.model.getUnits().get(i);
                if (unit instanceof SwitchDefaultBlock) {
                    if (depth == 0) {
                        return true;         // we are good!
                    } else {
                        depth--;
                    }
                } else if (unit instanceof SwitchDefaultBlockEnd) {
                    depth++;
                }
            }

            return false;
        }


        @Override
        public void enterComment(RockerParser.CommentContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            String text = ctx.getText();
            
            // trim leading @* and trailing *@
            String trimmedComment = text.substring(2, text.length()-2);
            
            model.getUnits().add(new Comment(sourceRef, trimmedComment));
        }
        
        @Override
        public void enterImportDeclaration(RockerParser.ImportDeclarationContext ctx) {
            verifyTemplateHeaderElementOK();
            
            SourceRef sourceRef = createSourceRef(ctx);
            
            // we only care about child import statement
            RockerParser.ImportStatementContext statementCtx = ctx.importStatement();
            
            // chop off 'import'
            String statement = statementCtx.getText().substring(6).trim();
            
            model.getImports().add(new JavaImport(sourceRef, statement));
        }

        @Override
        public void enterOptionDeclaration(RockerParser.OptionDeclarationContext ctx) {
            verifyTemplateHeaderElementOK();
            
            SourceRef sourceRef = createSourceRef(ctx);
            
            // we only care about child import statement
            RockerParser.OptionStatementContext statementCtx = ctx.optionStatement();
            
            // chop off 'option'
            String statement = statementCtx.getText().substring(6).trim();
            
            model.getOptions().parseOption(new Option(sourceRef, statement));
        }

        @Override
        public void exitArgumentsDeclaration(RockerParser.ArgumentsDeclarationContext ctx) {
            //log.info("template header completed: line={}", ctx.getStart().getLine());
        }

        @Override
        public void enterArgumentsStatement(RockerParser.ArgumentsStatementContext ctx) {
            verifyTemplateHeaderElementOK();
            
            SourceRef sourceRef = createSourceRef(ctx);
            
            // chop leading 'args'
            String statement = ctx.getText().substring(4).trim();
            
            if (!statement.startsWith("(") || !statement.endsWith(")")) {
               throw TemplateParser.buildParserException(sourceRef, templatePath, "Arguments for @args must be enclosed with parentheses");
            }
            
            // chomp off parenthese
            statement = statement.substring(1, statement.length() - 1);
            
            // fix for issue #17
            // remove leading and trailing spaces (might result in empty string, which is ok)
            // supports empty argument lists spanning over multiple lines
            statement = statement.replaceAll("\\s+", " ").trim();
            
            try {
                List args = JavaVariable.parseList(statement);
                
                // make sure each argument has a type
                for (JavaVariable arg : args) {
                    if (arg.getType() == null) {
                        throw new TokenException("Argument " + arg.getName() + " missing type");
                    }
                    
                    model.getArguments().add(new Argument(sourceRef, arg));
                }
            } catch (TokenException e) {
                throw TemplateParser.buildParserException(sourceRef, templatePath, e.getMessage(), e);
            }
            
            // special handling for argument of "RockerBody"
            for (int i = 0; i < model.getArguments().size(); i++) {
                Argument arg = model.getArguments().get(i);
                if (arg.getType().equals("RockerBody")) {
                    // only permitted as the LAST argument
                    if (i != (model.getArguments().size() - 1)) {
                        throw TemplateParser.buildParserException(sourceRef, templatePath, "RockerBody type only allowed as last argument");
                    }
                }
            }
            
        }
        
        @Override
        public void enterPlain(RockerParser.PlainContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            // is this plain in the context of a plainBlock?
            // we skip processing these since more plains will come that we prefer
            // to process instead
            if (ctx.plainBlock() != null) {
                log.trace("Plain but within context of PlainBlock -- skipping it!");
                return;
            }
            
            String text = ctx.getText();
            
            // unescape it (e.g. @@ -> @)
            String unescaped = PlainText.unescape(text);
            
            model.getUnits().add(new PlainText(sourceRef, unescaped));
        }

        @Override
        public void enterPlainBlock(RockerParser.PlainBlockContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            // we simply want to keep the left token for the start of this block
            String text = null;
            
            if (ctx.LCURLY() != null) {
                text = ctx.LCURLY().getText();
            } else {
                throw TemplateParser.buildParserException(sourceRef, templatePath, "Did not find LCURLY");
            }
            
            model.getUnits().add(new PlainText(sourceRef, text));
        }

        @Override
        public void exitPlainBlock(RockerParser.PlainBlockContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            // we simply want to keep the right token for the end of this block
            String text;
            
            if (ctx.RCURLY() != null) {
                text = ctx.RCURLY().getText();
            } else {
                // do nothing, no RCURLY found
                return;
            }
            
            model.getUnits().add(new PlainText(sourceRef, text));
        }

        @Override
        public void enterPlainElseIfBlock(final RockerParser.PlainElseIfBlockContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);

            model.getUnits().add(new PlainText(sourceRef, ctx.ELSE_IF().getText()));
        }

        @Override
        public void enterPlainElseBlock(RockerParser.PlainElseBlockContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            // we simply want to keep the left token for the start of this block
            String text;
            
            if (ctx.ELSE() != null) {
                text = ctx.ELSE().getText();
            } else {
                throw TemplateParser.buildParserException(sourceRef, templatePath, "Did not find ELSE");
            }
            
            model.getUnits().add(new PlainText(sourceRef, text));
        }

        @Override
        public void exitPlainElseBlock(RockerParser.PlainElseBlockContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            // we simply want to keep the right token for the end of this block
            String text;
            
            if (ctx.RCURLY() != null) {
                text = ctx.RCURLY().getText();
            } else {
                // do nothing, no RCURLY found
                return;
            }
            
            model.getUnits().add(new PlainText(sourceRef, text));
        }

        @Override
        public void enterValueClosure(RockerParser.ValueClosureContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            // we only care about the expression
            RockerParser.ValueClosureExpressionContext expressionCtx = ctx.valueClosureExpression();
            
            String expr = expressionCtx.getText();
            
            // we need to chomp off the " -> {" at the end
            expr = RockerUtil.chompClosureOpen(expr);
            
            model.getUnits().add(new ValueClosureBegin(sourceRef, expr));
        }

        @Override
        public void exitValueClosure(RockerParser.ValueClosureContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            model.getUnits().add(new ValueClosureEnd(sourceRef));
        }
        
        @Override
        public void enterContentClosure(RockerParser.ContentClosureContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            // we only care about the expression
            RockerParser.ContentClosureExpressionContext expressionCtx = ctx.contentClosureExpression();
            
            String expr = expressionCtx.getText();
            
            // we need to chomp off the " => {" at the end
            String identifier = RockerUtil.chompClosureAssignmentOpen(expr);
            
            model.getUnits().add(new ContentClosureBegin(sourceRef, identifier));
        }

        @Override
        public void exitContentClosure(RockerParser.ContentClosureContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            model.getUnits().add(new ContentClosureEnd(sourceRef));
        } 

        @Override
        public void enterValue(RockerParser.ValueContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            // we only care about the expression
            RockerParser.ValueExpressionContext expressionCtx = ctx.valueExpression();
            
            String expr = expressionCtx.getText();
            
            // speak handling for specific values which actually are commands
            // break, continue
            if (expr.equals("break")) {
                // verify we're in a "for" loop that hasn't ended yet...
                if (!areWeCurrentlyInAForLoop() && !areWeCurrentlyInASwitchBlock()) {
                    throw new ParserRuntimeException(sourceRef, "@break used outside @for loop OR @switch block", null);
                }
                model.getUnits().add(new BreakStatement(sourceRef));
            } else if (expr.equals("continue")) {
                if (!areWeCurrentlyInAForLoop()) {
                    throw new ParserRuntimeException(sourceRef, "@continue used outside @for loop", null);
                }
                model.getUnits().add(new ContinueStatement(sourceRef));
            } else {
                // null safety?
                boolean nullSafety = false;
                if (expr.startsWith("?")) {
                    nullSafety = true;
                    expr = expr.substring(1);   // chop it off
                }

                // if next char is a '(' then we're 
                
                model.getUnits().add(new ValueExpression(sourceRef, expr, nullSafety));
            }
        }
        
        @Override
        public void enterNullTernary(RockerParser.NullTernaryContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            // we only care about the expressions
            RockerParser.NullTernaryExpressionContext nullTernaryExpr = ctx.nullTernaryExpression();
            
            String leftExpr = nullTernaryExpr.MV_NULL_TERNARY_LH().getText();
            // chop off trailing "?:"
            leftExpr = leftExpr.substring(0, leftExpr.length()-2);
            
            String rightExpr = nullTernaryExpr.MV_NULL_TERNARY_RH().getText();
            
            model.getUnits().add(new NullTernaryExpression(sourceRef, leftExpr, rightExpr));
        }

        @Override
        public void enterEval(RockerParser.EvalContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            // we only care about the expression
            RockerParser.EvalExpressionContext evalExpr = ctx.evalExpression();
            
            String expr = evalExpr.getText();
            
            model.getUnits().add(new EvalExpression(sourceRef, expr, false));
        }

        @Override
        public void enterForBlock(RockerParser.ForBlockContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            // "for(..){" or "for (...) {"
            String expr = ctx.MV_FOR().getText();
            
            // chop off leading 'for' and trailing '{' and then leading/trailing whitespace
            expr = expr.substring(3, expr.length() - 1).trim();
            
            try {
                ForStatement statement = ForStatement.parse(expr);

                // any Java 1.8+ features used?
                if (!isJava8Plus(model) && statement.hasAnyUntypedArguments()) {
                    throw new TokenException("Untyped variables cannot be used with Java " + model.getOptions().getJavaVersion().getLabel() + " (only allowed with Java 1.8+)");
                }
                
                model.getUnits().add(new ForBlockBegin(sourceRef, expr, statement));
            } catch (TokenException e) {
                throw TemplateParser.buildParserException(sourceRef, templatePath, e.getMessage(), e);
            }
        }

        @Override
        public void exitForBlock(RockerParser.ForBlockContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            model.getUnits().add(new ForBlockEnd(sourceRef));
        }
        
        @Override
        public void enterWithBlock(RockerParser.WithBlockContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            // "with (...) {"
            String expr = ctx.MV_WITH().getText();
            
            // chop off leading 'with' and trailing '{' and then leading/trailing whitespace
            expr = expr.substring(4, expr.length() - 1).trim();
            
            try {
                WithStatement statement = WithStatement.parse(expr, templatePath);

                // any Java 1.8+ features used?
                if (!isJava8Plus(model) && statement.hasAnyVariableNullType()) {
                    throw new TokenException("Untyped variables cannot be used with Java " + model.getOptions().getJavaVersion().getLabel() + " (only allowed with Java 1.8+)");
                }
                
                model.getUnits().add(new WithBlockBegin(sourceRef, expr, statement));

                // Put it on the stack for checking in the exitWithBlock part.
                withStatements.push(statement);
            } catch (TokenException e) {
                throw TemplateParser.buildParserException(sourceRef, templatePath, e.getMessage(), e);
            }
        }

        @Override
        public void enterWithElseBlock(RockerParser.WithElseBlockContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            model.getUnits().add(new WithBlockElse(sourceRef));

            // Check that the withStatement is only with one argument or the else is not allowed.
            // Also if the statement has 1 argument, but is not an null-safe it is not allowed either.
            WithStatement withStatement = withStatements.peek();

            if(withStatement.getVariables().size() > 1) {
                throw TemplateParser.buildParserException(sourceRef, templatePath, "Cannot have an else statement for a @with block with multiple arguments");
            }
            else if(!withStatement.isNullSafe()) {
                throw TemplateParser.buildParserException(sourceRef, templatePath, "Cannot have an else statement for a @with block that is not null-safe");
            }
        }

        @Override
        public void exitWithBlock(RockerParser.WithBlockContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            model.getUnits().add(new WithBlockEnd(sourceRef));

            withStatements.pop();
        }
        
        @Override
        public void enterIfBlock(RockerParser.IfBlockContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            // only need the expression
            // "if(b){" or "if (b) {"
            String expr = ctx.MV_IF().getText();
            
            // chop off leading 'if' and trailing '{' and then leading/trailing whitespace
            expr = expr.substring(2, expr.length() - 1).trim();
            
            model.getUnits().add(new IfBlockBegin(sourceRef, expr));
        }

        @Override
        public void exitIfBlock(RockerParser.IfBlockContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            model.getUnits().add(new IfBlockEnd(sourceRef));
        }

        @Override
        public void enterIfElseIfBlock(RockerParser.IfElseIfBlockContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);

            // ELSE_IF
            //    :   '}' Ws? 'else' Ws? 'if' Ws? Parentheses Ws? '{'

            // Find the parentheses (first and last)
            final String text = ctx.ELSE_IF().getText();
            final int idxFirst = text.indexOf('(');
            final int idxLast = text.lastIndexOf(')');
            final String expression = text.substring(idxFirst, idxLast+1);
            model.getUnits().add(new IfBlockElseIf(sourceRef, expression));
        }

        @Override
        public void enterIfElseBlock(RockerParser.IfElseBlockContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            
            model.getUnits().add(new IfBlockElse(sourceRef));
        }

        @Override
        public void enterSwitchBlock(RockerParser.SwitchBlockContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            final String text = ctx.MV_SWITCH().getText();
            final int idxFirst = text.indexOf('(');
            final int idxLast = text.lastIndexOf(')');
            final String expression = text.substring(idxFirst, idxLast + 1);
            model.getUnits().add(new SwitchBlock(sourceRef, expression));
        }

        @Override
        public void exitSwitchBlock(RockerParser.SwitchBlockContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            model.getUnits().add(new SwitchBlockEnd(sourceRef));
            verifySwitchBlock();
        }

        @Override
        public void enterSwitchExpressionBlock(RockerParser.SwitchExpressionBlockContext ctx) {

            SourceRef sourceRef = createSourceRef(ctx);
            final String text = ctx.MV_SWITCH().getText();
            final int idxFirst = text.indexOf('(');
            final int idxLast = text.lastIndexOf(')');
            final String expression = text.substring(idxFirst, idxLast + 1);
            model.getUnits().add(new SwitchExpressionBlock(sourceRef, expression));
        }

        @Override
        public void exitSwitchExpressionBlock(RockerParser.SwitchExpressionBlockContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            model.getUnits().add(new SwitchExpressionBlockEnd(sourceRef));
            verifySwitchExpressionBlock();
        }

        @Override
        public void enterSwitchExpressionCase(RockerParser.SwitchExpressionCaseContext ctx) {

            SourceRef sourceRef = createSourceRef(ctx);
            final String text = ctx.CASE_EXPRESSION().getText();
            final int idxFirst = text.indexOf('(');
            final int idxLast = text.lastIndexOf(')');
            final String expression = text.substring(idxFirst+1, idxLast );
            model.getUnits().add(new SwitchCaseExpressionBlock(sourceRef, expression));

        }

        @Override
        public void exitSwitchExpressionCase(RockerParser.SwitchExpressionCaseContext ctx) {
             SourceRef sourceRef = createSourceRef(ctx);
            model.getUnits().add(new SwitchCaseExpressionBlockEnd(sourceRef));
        }

        @Override
        public void enterSwitchExpressionDefault(RockerParser.SwitchExpressionDefaultContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            model.getUnits().add(new SwitchDefaultExpressionBlock(sourceRef));
        }

        @Override
        public void exitSwitchExpressionDefault(RockerParser.SwitchExpressionDefaultContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            model.getUnits().add(new SwitchDefaultExpressionBlockEnd(sourceRef));

        }

        private void verifySwitchBlock() {

            List units = this.model.getUnits();
            List toRemove=new ArrayList<>();
            for (int i = 0, unitsSize = units.size(); i < unitsSize; i++) {
                TemplateUnit unit = units.get(i);
                    if (unit instanceof PlainText) {
                        PlainText plain = (PlainText) unit;
                        if (inSwitchButNotCaseOrDefault(i)) {
                            if (plain.isWhitespace()) {
                                toRemove.add(unit);
                            } else {
                                // no plain allowed
                                SourcePosition pos = plain.findSourcePositionOfNonWhitespace();
                                throw new ParserRuntimeException(pos.getLineNumber(), pos.getPosInLine(), "plain text not allowed before end of switch block");
                            }
                        }
                    }
            }
            units.removeAll(toRemove);
        }
        private void verifySwitchExpressionBlock() {

            List units = this.model.getUnits();
            List toRemove=new ArrayList<>();
            for (int i = 0, unitsSize = units.size(); i < unitsSize; i++) {
                TemplateUnit unit = units.get(i);
                if (unit instanceof PlainText) {
                    PlainText plain = (PlainText) unit;
                    if (inSwitchExpressionButNotCaseOrDefault(i)) {
                        if (plain.isWhitespace()) {
                            toRemove.add(unit);
                        } else {
                            // no plain allowed
                            SourcePosition pos = plain.findSourcePositionOfNonWhitespace();
                            throw new ParserRuntimeException(pos.getLineNumber(), pos.getPosInLine(), "plain text not allowed before end of switch block");
                        }
                    }
                }
            }
            units.removeAll(toRemove);
        }

        private boolean inSwitchButNotCaseOrDefault(int i) {
            for (int j = i; j >= 0; j--) {
                TemplateUnit templateUnit = model.getUnits().get(j);
                if (templateUnit instanceof SwitchCaseBlockEnd || templateUnit instanceof SwitchDefaultBlockEnd || templateUnit instanceof SwitchBlock) {
                    return true;
                }
                if (templateUnit instanceof SwitchCaseBlock || templateUnit instanceof SwitchDefaultBlock || templateUnit instanceof SwitchBlockEnd) {
                    return false;
                }
            }
            return false;
        }
        private boolean inSwitchExpressionButNotCaseOrDefault(int i) {
            for (int j = i; j >= 0; j--) {
                TemplateUnit templateUnit = model.getUnits().get(j);
                if (templateUnit instanceof SwitchCaseExpressionBlockEnd || templateUnit instanceof SwitchDefaultExpressionBlockEnd || templateUnit instanceof SwitchExpressionBlock) {
                    return true;
                }
                if (templateUnit instanceof SwitchCaseExpressionBlock || templateUnit instanceof SwitchDefaultExpressionBlock || templateUnit instanceof SwitchExpressionBlockEnd) {
                    return false;
                }
            }
            return false;
        }

        @Override
        public void enterSwitchCase(RockerParser.SwitchCaseContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            final String text = ctx.CASE().getText();
            final int idxFirst = text.indexOf('(');
            final int idxLast = text.lastIndexOf(')');
            final String expression = text.substring(idxFirst+1, idxLast );
            model.getUnits().add(new SwitchCaseBlock(sourceRef, expression));
        }

        @Override
        public void exitSwitchCase(RockerParser.SwitchCaseContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            model.getUnits().add(new SwitchCaseBlockEnd(sourceRef));
        }

        @Override
        public void enterSwitchDefault(RockerParser.SwitchDefaultContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            model.getUnits().add(new SwitchDefaultBlock(sourceRef));
        }

        @Override
        public void exitSwitchDefault(RockerParser.SwitchDefaultContext ctx) {
            SourceRef sourceRef = createSourceRef(ctx);
            model.getUnits().add(new SwitchDefaultBlockEnd(sourceRef));
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy