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

cz.vutbr.web.csskit.antlr4.CSSParserVisitorImpl Maven / Gradle / Ivy

package cz.vutbr.web.csskit.antlr4;

import cz.vutbr.web.css.*;
import cz.vutbr.web.css.Selector.PseudoElement;
import cz.vutbr.web.csskit.RuleArrayList;
import cz.vutbr.web.csskit.antlr4.CSSParser.Bracketed_identsContext;
import cz.vutbr.web.csskit.antlr4.CSSParser.Ident_list_itemContext;
import cz.vutbr.web.csskit.antlr4.CSSParser.Keyframe_blockContext;
import cz.vutbr.web.csskit.antlr4.CSSParser.Keyframe_selectorContext;
import cz.vutbr.web.csskit.antlr4.CSSParser.Keyframes_nameContext;

import org.antlr.v4.runtime.CommonToken;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.RuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.*;

import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;


public class CSSParserVisitorImpl implements CSSParserVisitor, CSSParserExtractor {
    // factories for building structures
    private RuleFactory rf = CSSFactory.getRuleFactory();
    private TermFactory tf = CSSFactory.getTermFactory();

    private enum MediaQueryState {START, TYPE, AND, EXPR, TYPEOREXPR}

    //logger
    private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(getClass());
    //counter of spaces for pretty debug printing
    private int spacesCounter = 0;

    // block preparator
    private Preparator preparator;

    // list of media queries to wrap rules
    private List wrapMedia;

    // structures after parsing
    private List importPaths = new ArrayList<>();
    private List> importMedia = new ArrayList<>();
    private RuleList rules = null;
    private List mediaQueryList = null;

    //prevent imports inside the style sheet
    private boolean preventImports = false;

    private void logEnter(String entry) {
        if (log.isTraceEnabled())
            log.trace("Enter: {}{}", generateSpaces(spacesCounter), entry);
    }

    private void logEnter(String entry, RuleContext ctx) {
        if (log.isTraceEnabled())
            log.trace("Enter: {}{}: >{}<", generateSpaces(spacesCounter), entry, ctx.getText());
    }
    
    private void logLeave(String leaving) {
        if (log.isTraceEnabled())
            log.trace("Leave: {}{}", generateSpaces(spacesCounter), leaving);
    }

    private String extractTextUnescaped(String text) {
        return org.unbescape.css.CssEscape.unescapeCss(text);
    }

    private Declaration.Source extractSource(CSSToken ct) {
        return new Declaration.Source(ct.getBase(), ct.getLine(), ct.getCharPositionInLine());
    }

    /**
     * extract base from parse tree node
     */
    private URL extractBase(TerminalNode node) {
        CSSToken ct = (CSSToken) node.getSymbol();
        return ct.getBase();
    }

    /**
     * check if string is valid ID
     *
     * @param id ID to validate and unescapes
     * @return unescaped id or null
     */
    private String extractIdUnescaped(String id) {
        if (!id.isEmpty() && !Character.isDigit(id.charAt(0))) {
            return org.unbescape.css.CssEscape.unescapeCss(id);
        }
        return null;
    }

    /**
     * generate spaces for pretty debug printing
     *
     * @param count number of generated spaces
     * @return string with spaces
     */
    private String generateSpaces(int count) {
        String spaces = "";
        for (int i = 0; i < count; i++) {
            spaces += " ";
        }
        return spaces;
    }

    /**
     * remove terminal node emtpy tokens from input list
     *
     * @param inputArrayList original list
     * @return list without terminal node type = S (space)
     */
    private List filterSpaceTokens(List inputArrayList) {
        List ret = new ArrayList(inputArrayList.size());
        for (ParseTree item : inputArrayList) {
            if (!(item instanceof TerminalNode) || ((TerminalNodeImpl) item).getSymbol().getType() != CSSLexer.S) {
                ret.add(item);
            }
        }
        return ret;
    }

    /**
     * check if rule context contains error node
     *
     * @param ctx rule context
     * @return contains context error node
     */
    private boolean ctxHasErrorNode(ParserRuleContext ctx) {
        for (int i = 0; i < ctx.children.size(); i++) {
            if (ctx.getChild(i) instanceof ErrorNode) {
                return true;
            }
        }
        return false;
    }

    /**
     * Tries to convert generic terms to more specific value types. Currently, colors (TermColor) and
     * rectangles (TermRect) are supported.
     * @param term the term to be converted
     * @return the corresponding more specific term type or {@code null} when nothing was found.
     */
    private Term findSpecificType(Term term)
    {
        TermColor colorTerm = null;
        TermRect rectTerm = null;
        if (term instanceof TermIdent) { //idents - try to convert colors
            colorTerm = tf.createColor((TermIdent) term);
        } else if (term instanceof TermFunction) { // rgba(0,0,0)
            colorTerm = tf.createColor((TermFunction) term);
            if (colorTerm == null)
                rectTerm = tf.createRect((TermFunction) term);
        }
        //replace with more specific value
        if (colorTerm != null) {
            if (log.isDebugEnabled()) {
                log.debug("term color is OK - creating - " + colorTerm.toString());
            }
            return colorTerm;
        } else if (rectTerm != null) {
            if (log.isDebugEnabled()) {
                log.debug("term rect is OK - creating - " + rectTerm.toString());
            }
            return rectTerm;
        } else
            return null;
    }
    
    /**
     * get parsed rulelist
     *
     * @return parsed rules
     */
    public RuleList getRules() {
        return rules;
    }

    /**
     * get mediaquery list
     *
     * @return media query list
     */
    public List getMedia() {
        return mediaQueryList;
    }

    /**
     * get import list
     *
     * @return list of urls to import
     */
    public List getImportPaths() {
        return importPaths;
    }

    /**
     * get media for imports
     *
     * @return list of media for imports
     */
    public List> getImportMedia() {
        return importMedia;
    }


    /**
     * Constructor
     *
     * @param preparator The preparator to be used for creating the rules.
     * @param wrapMedia  The media queries to be used for wrapping the created rules (e.g. in case
     *                   of parsing and imported style sheet) or null when no wrapping is required.
     */
    public CSSParserVisitorImpl(Preparator preparator, List wrapMedia) {
        this.preparator = preparator;
        this.wrapMedia = wrapMedia;
    }

    //used in parseMediaQuery
    public CSSParserVisitorImpl() {

    }
    /******************************************************************
     /******************************************************************
     /******************************************************************
     /******************************************************************
     /******************************************************************
     /******************************************************************
     /******************************************************************
     /******************************************************************
     /******************************************************************/


    /**
     * @param ctx the parse tree
     * @return RuleList
     * inlinestyle: S* (declarations | inlineset+ )
     */
    @Override
    public RuleList visitInlinestyle(CSSParser.InlinestyleContext ctx) {
        logEnter("inlinestyle");
        this.rules = new cz.vutbr.web.csskit.RuleArrayList();
        if (ctx.declarations() != null) {
            //declarations
            List decl = visitDeclarations(ctx.declarations());
            cz.vutbr.web.css.RuleBlock rb = preparator.prepareInlineRuleSet(decl, null);
            if (rb != null) {
                //rb is valid,add to rules
                this.rules.add(rb);
            }
        } else {
            //inlineset
            for (CSSParser.InlinesetContext ctxis : ctx.inlineset()) {
                cz.vutbr.web.css.RuleBlock irs = visitInlineset(ctxis);
                if (irs != null) {
                    //irs is valid, add to rules
                    this.rules.add(irs);
                }
            }
        }
        if (log.isDebugEnabled()) {
            log.debug("\n***\n{}\n***\n", this.rules);
        }
        logLeave("inlinestyle");
        return this.rules;
    }

    /**
     * Stylesheet, main rule
     * stylesheet: ( CDO | CDC  | S | nostatement | statement )*
     * statement* is only processed
     */
    @Override
    public RuleList visitStylesheet(CSSParser.StylesheetContext ctx) {
        logEnter("stylesheet: ", ctx);
        this.rules = new RuleArrayList();
        //statement*
        for (CSSParser.StatementContext stmt : ctx.statement()) {
            RuleBlock s = visitStatement(stmt);
            if (s != null) {
                //add statement to rules
                this.rules.add(s);
            }
        }
        if (log.isDebugEnabled()) {
            log.debug("\n***\n{}\n***\n", this.rules);
        }
        logLeave("stylesheet");
        return this.rules;
    }

    /**
     * scope and stack for statement
     * - this is for accessing statement scope from inner rules
     * e.g. - used for invalidate statement from selector
     */
    protected static class statement_scope {
        boolean invalid = false;
    }

    /**
     * stack for posibly recursion
     */
    protected Stack statement_stack = new Stack<>();

    @Override
    /**
     * Statement, main contents unit
     * statement : ruleset | atstatement
     */
    public RuleBlock visitStatement(CSSParser.StatementContext ctx) {
        if (ctxHasErrorNode(ctx)) {
            //context is invalid
            return null;
        }
        logEnter("statement: ", ctx);
        //create new scope and push it to stack
        statement_stack.push(new statement_scope());
        RuleBlock stmt = null;
        if (ctx.ruleset() != null) {
            //ruleset
            stmt = visitRuleset(ctx.ruleset());
        } else if (ctx.atstatement() != null) {
            //atstatement
            stmt = visitAtstatement(ctx.atstatement());
        }
        if (statement_stack.peek().invalid) {
            //stmt == null - is invalid
            if (log.isDebugEnabled()) {
                log.debug("Statement is invalid");
            }
        }
        statement_stack.pop();
        logLeave("statement");
        //could be null
        return stmt;
    }

    @Override
    /**
     *

     atstatement
     : CHARSET
     | IMPORT S* import_uri S* media? SEMICOLON
     | page
     | VIEWPORT S* LCURLY S* declarations RCURLY
     | FONTFACE S* LCURLY S* declarations RCURLY
     | MEDIA S* media? LCURLY S* (media_rule S*)* RCURLY
     | unknown_atrule
     ;

     */
    public RuleBlock visitAtstatement(CSSParser.AtstatementContext ctx) {
        logEnter("atstatement: ", ctx);
        RuleBlock atstmt = null;
        //noinspection StatementWithEmptyBody
        if (ctx.CHARSET() != null) {
            //charset is served in lexer
        }
        //import
        else if (ctx.IMPORT() != null) {
            List im = null;
            if (ctx.media() != null) {
                im = visitMedia(ctx.media());
            }
            ctx.import_uri();
            String iuri = visitImport_uri(ctx.import_uri());
            if (!this.preventImports && iuri != null) {
                if (log.isDebugEnabled()) {
                    log.debug("Adding import: {}", iuri);
                }
                importMedia.add(im);
                importPaths.add(iuri);
            } else {
                if (log.isDebugEnabled()) {
                    log.debug("Ignoring import: {}", iuri);
                }
            }
        }
        //page
        else if (ctx.page() != null) {

            atstmt = visitPage(ctx.page());
        }
        //viewport
        else if (ctx.VIEWPORT() != null) {

            List declarations = visitDeclarations(ctx.declarations());
            atstmt = preparator.prepareRuleViewport(declarations);
            if (atstmt != null)
                this.preventImports = true;
        }
        //fontface
        else if (ctx.FONTFACE() != null) {

            List declarations = visitDeclarations(ctx.declarations());
            atstmt = preparator.prepareRuleFontFace(declarations);
            if (atstmt != null)
                this.preventImports = true;
        }
        //media
        else if (ctx.MEDIA() != null) {

            List mediaList = null;
            List rules = null;
            if (ctx.media() != null) {
                mediaList = visitMedia(ctx.media());
            }
            if (ctx.media_rule() != null) {
                rules = new ArrayList<>();
                for (CSSParser.Media_ruleContext mr : ctx.media_rule()) {
                    RuleBlock rs = visitMedia_rule(mr);
                    if (rs != null) {
                        rules.add((RuleSet) rs);
                    }
                }
            }
            atstmt = preparator.prepareRuleMedia(rules, mediaList);
            if (atstmt != null)
                this.preventImports = true;
        }
        //keyframes
        else if (ctx.KEYFRAMES() != null) {
            String name = null;
            List keyframes = null;
            if (ctx.keyframes_name() != null) {
                name = visitKeyframes_name(ctx.keyframes_name());
            }
            if (ctx.keyframe_block() != null) {
                keyframes = new ArrayList<>();
                for (CSSParser.Keyframe_blockContext kfctx : ctx.keyframe_block()) {
                    KeyframeBlock block = visitKeyframe_block(kfctx);
                    if (block != null) {
                        keyframes.add(block);
                    }
                }
            }
            atstmt = preparator.prepareRuleKeyframes(keyframes, name);
            if (atstmt != null)
                this.preventImports = true;
        }
        //unknown
        else {
            if (log.isDebugEnabled()) {
                log.debug("Skipping invalid at statement");
            }
        }
        logLeave("atstatement");
        return atstmt;
    }

    @Override
    /**
     * import_uri : (STRING | URI)
     */
    public String visitImport_uri(CSSParser.Import_uriContext ctx) {
        if (ctx != null)
            return extractTextUnescaped(ctx.getText());
        else
            return null;
    }

    @Override
    /**
     *
     page
     : PAGE S* IDENT? pseudo? S*
     LCURLY S*
     declarations margin_rule*
     RCURLY
     */
    public RuleBlock visitPage(CSSParser.PageContext ctx) {
        boolean invalid = false;
        String name = null;
        if (ctx.IDENT() != null) {
            name = extractTextUnescaped(ctx.IDENT().getText());
        }
        Selector.PseudoPage pseudo = null;
        if (ctx.pseudo() != null) {
            Selector.SelectorPart p = visitPseudo(ctx.pseudo());
            if (p != null && p instanceof Selector.PseudoPage) {
                pseudo = (Selector.PseudoPage) p;
            } else { // Invalid pseudo
                if (log.isDebugEnabled()) {
                    log.debug("skipping RulePage with invalid pseudo-class: " + pseudo);
                }
                invalid = true;
            }
        }
        List declarations = visitDeclarations(ctx.declarations());
        List margins = null;
        if (ctx.margin_rule() != null) {
            margins = new ArrayList<>();
            for (CSSParser.Margin_ruleContext mctx : ctx.margin_rule()) {
                RuleMargin m = visitMargin_rule(mctx);
                margins.add(m);
                if (log.isDebugEnabled()) {
                    log.debug("Inserted margin rule #{} into @page", margins.size() + 1);
                }
            }
        }

        if (invalid) {
            return null;
        } else {
            RuleBlock rb = preparator.prepareRulePage(declarations, margins, name, pseudo);
            if (rb != null)
                this.preventImports = true;
            return rb;
        }
    }

    @Override
    /**
     * margin_rule : MARGIN_AREA S* LCURLY S* declarations RCURLY S*
     */
    public RuleMargin visitMargin_rule(CSSParser.Margin_ruleContext ctx) {
        logEnter("margin_rule");
        RuleMargin m;
        String area = ctx.MARGIN_AREA().getText();
        List decl = visitDeclarations(ctx.declarations());
        m = preparator.prepareRuleMargin(extractTextUnescaped(area).substring(1), decl);
        logLeave("margin_rule");
        return m;
    }

    @Override
    /**
     *
     inlineset
     : (pseudo S* (COMMA S* pseudo S*)*)?
     LCURLY
     declarations
     RCURLY
     ;
     */
    public cz.vutbr.web.css.RuleBlock visitInlineset(CSSParser.InlinesetContext ctx) {
        logEnter("inlineset");
        List pplist = new ArrayList<>();
        if (ctx.pseudo() != null) {
            for (CSSParser.PseudoContext pctx : ctx.pseudo()) {
                Selector.SelectorPart p = visitPseudo(pctx);
                pplist.add(p);
            }
        }
        List decl = visitDeclarations(ctx.declarations());
        RuleBlock is = preparator.prepareInlineRuleSet(decl, pplist);
        logLeave("inlineset");
        return is;
    }

    @Override
    /**
     media : media_query (COMMA S* media_query)*
     */
    public List visitMedia(CSSParser.MediaContext ctx) {
        logEnter("media: ", ctx);
        List queries = mediaQueryList = new ArrayList<>();
        for (CSSParser.Media_queryContext mqc : ctx.media_query()) {
            queries.add(visitMedia_query(mqc));
        }
        if (log.isDebugEnabled()) {
            log.debug("Totally returned {} media queries.", queries.size());
        }
        logLeave("media");
        return queries;
    }

    protected static class mediaquery_scope {
        cz.vutbr.web.css.MediaQuery q;
        MediaQueryState state;
        boolean invalid;
    }

    mediaquery_scope mq;

    @Override
    /**
     * media_query : (media_term S*)+
     */
    public MediaQuery visitMedia_query(CSSParser.Media_queryContext ctx) {
        logEnter("mediaquery: ", ctx);
        mq = new mediaquery_scope();
        mq.q = rf.createMediaQuery();
        mq.q.unlock();
        mq.state = MediaQueryState.START;
        mq.invalid = false;
        logLeave("mediaquery");
        for (CSSParser.Media_termContext mtc : ctx.media_term()) {
            visitMedia_term(mtc);
        }
        if (mq.invalid) {
            log.trace("Skipping invalid rule {}", mq.q);
            mq.q.setType("all"); //change the malformed media queries to "not all"
            mq.q.setNegative(true);
        }
        logLeave("mediaquery");
        return mq.q;
    }

    @Override
    /**
     *
     media_term
     : (IDENT | media_expression)
     | nomediaquery
     */
    public Object visitMedia_term(CSSParser.Media_termContext ctx) {
        //IDENT
        if (ctx.IDENT() != null) {
            String m = extractTextUnescaped(ctx.IDENT().getText());
            MediaQueryState state = mq.state;
            if (m.equalsIgnoreCase("ONLY") && state == MediaQueryState.START) {
                mq.state = MediaQueryState.TYPEOREXPR;
            } else if (m.equalsIgnoreCase("NOT") && state == MediaQueryState.START) {
                mq.q.setNegative(true);
                mq.state = MediaQueryState.TYPEOREXPR;
            } else if (m.equalsIgnoreCase("AND") && state == MediaQueryState.AND) {
                mq.state = MediaQueryState.EXPR;
            } else if (state == MediaQueryState.START
                    || state == MediaQueryState.TYPE
                    || state == MediaQueryState.TYPEOREXPR) {
                mq.q.setType(m);
                mq.state = MediaQueryState.AND;
            } else {
                log.trace("Invalid media query: found ident: {} state: {}", m, state);
                mq.invalid = true;
            }
        }
        //media_expression
        else if (ctx.media_expression() != null) {
            MediaExpression e = visitMedia_expression(ctx.media_expression());
            if (mq.state == MediaQueryState.START
                    || mq.state == MediaQueryState.EXPR
                    || mq.state == MediaQueryState.TYPEOREXPR) {
                if (e != null && e.getFeature() != null) //the expression is valid
                {
                    mq.q.add(e);
                    mq.state = MediaQueryState.AND;
                } else {
                    log.trace("Invalidating media query for invalud expression");
                    mq.invalid = true;
                }
            } else {
                log.trace("Invalid media query: found expr, state: {}", mq.state);
                mq.invalid = true;
            }
        }
        //nomediaquery
        else {
            mq.invalid = true;
        }
        return null;
    }

    @Override
    /**
     * media_expression : LPAREN S* IDENT S* (COLON S* terms)? RPAREN
     */
    public MediaExpression visitMedia_expression(CSSParser.Media_expressionContext ctx) {
        logEnter("mediaexpression: ", ctx);
        if (ctxHasErrorNode(ctx)) {
            mq.invalid = true;
            return null;
        }
        MediaExpression expr = rf.createMediaExpression();
        Declaration decl;
        declaration_stack.push(new declaration_scope());
        declaration_stack.peek().d = decl = rf.createDeclaration();
        declaration_stack.peek().invalid = false;

        String property = extractTextUnescaped(ctx.IDENT().getText());
        decl.setProperty(property);
        Token token = ctx.IDENT().getSymbol();
        decl.setSource(extractSource((CSSToken) token));
        if (ctx.terms() != null) {
            List> t = visitTerms(ctx.terms());
            decl.replaceAll(t);
        }

        if (declaration_stack.peek().d != null && !declaration_stack.peek().invalid) { //if the declaration is valid
            expr.setFeature(decl.getProperty());
            expr.replaceAll(decl);
        }
        declaration_stack.pop();

        logLeave("mediaexpression");
        return expr;
    }

    @Override
    /**
     *
     media_rule
     : ruleset
     | atstatement //invalid statement
     ;
     */
    public RuleBlock visitMedia_rule(CSSParser.Media_ruleContext ctx) {
        logEnter("media_rule: ", ctx);
        RuleBlock rules = null;
        if (ctx.ruleset() != null) {
            statement_stack.push(new statement_scope());
            rules = visitRuleset(ctx.ruleset());
            statement_stack.pop();
        } else {
            if (log.isDebugEnabled()) {
                log.debug("Skiping invalid statement in media");
            }
        }
        logLeave("media_rule");
        //could be null
        return rules;
    }

    @Override
    public String visitKeyframes_name(Keyframes_nameContext ctx) {
        if (ctx.IDENT() != null)
            return extractTextUnescaped(ctx.IDENT().getText());
        else if (ctx.string() != null)
            return visitString(ctx.string());
        else
            return null;
    }

    @Override
    public KeyframeBlock visitKeyframe_block(Keyframe_blockContext ctx) {
        List selectors = null;
        if (ctx.keyframe_selector() != null) {
            selectors = new ArrayList<>();
            for (Keyframe_selectorContext selctx : ctx.keyframe_selector()) {
                TermPercent perc = visitKeyframe_selector(selctx);
                if (perc != null)
                    selectors.add(perc);
            }
        }

        List declarations = null;
        if (ctx.declarations() != null) {
            statement_stack.push(new statement_scope());
            declarations = visitDeclarations(ctx.declarations());
            statement_stack.pop();
        }
        
        if (declarations != null && selectors != null && !selectors.isEmpty()) {
            KeyframeBlock block = rf.createKeyframeBlock();
            block.setPercentages(selectors);
            block.replaceAll(declarations);
            return block;
        } else {
            return null;
        }
    }

    @Override
    public TermPercent visitKeyframe_selector(Keyframe_selectorContext ctx) {
        if (ctx.IDENT() != null) {
            final String idtext = ctx.IDENT().getText();
            if (idtext != null) {
                if (idtext.equalsIgnoreCase("from")) {
                    return tf.createPercent(0.0f);
                } else if (idtext.equalsIgnoreCase("to")) {
                    return tf.createPercent(100.0f);
                } else {
                    return null;
                }
            } else {
                return null;
            }
        } else if (ctx.PERCENTAGE() != null) {
            return tf.createPercent(ctx.PERCENTAGE().getText(), 1);
        } else {
            return null;
        }
    }

    @Override
    public Object visitUnknown_atrule(CSSParser.Unknown_atruleContext ctx) {
        //done in atstatement else section
        return null;
    }

    @Override
    public Object visitUnknown_atrule_body(CSSParser.Unknown_atrule_bodyContext ctx) {
        //not used - the unknown atrules are skipped
        return null;
    }
    
    @Override
    /**
     * The most common block in CSS file,
     * set of declarations with selector
     ruleset
     : combined_selector (COMMA S* combined_selector)*
     LCURLY S*
     declarations
     RCURLY
     | norule
     */
    public RuleBlock visitRuleset(CSSParser.RulesetContext ctx) {
        logEnter("ruleset");
        if (ctxHasErrorNode(ctx) || ctx.norule() != null) {
            log.trace("Leaving ruleset with error {} {}", ctxHasErrorNode(ctx), (ctx.norule() != null)); 
            return null;
        }
        List cslist = new ArrayList<>();
        // body
        for (CSSParser.Combined_selectorContext csctx : ctx.combined_selector()) {
            CombinedSelector cs = visitCombined_selector(csctx);
            if (cs != null && !cs.isEmpty() && !statement_stack.peek().invalid) {
                cslist.add(cs);
                if (log.isDebugEnabled()) {
                    log.debug("Inserted combined selector ({}) into ruleset", cslist.size());
                }
            }
        }
        List decl = visitDeclarations(ctx.declarations());
        RuleBlock stmnt;
        if (statement_stack.peek().invalid) {
            stmnt = null;
            if (log.isDebugEnabled()) {
                log.debug("Ruleset not valid, so not created");
            }
        } else {
            stmnt = preparator.prepareRuleSet(cslist, decl, (this.wrapMedia != null && !this.wrapMedia.isEmpty()), this.wrapMedia);
            this.preventImports = true;
        }
        logLeave("ruleset");
        return stmnt;
    }

    @Override
    /**
     * Multiple CSS declarations
     * declarations : declaration? (SEMICOLON S* declaration? )*
     */
    public List visitDeclarations(CSSParser.DeclarationsContext ctx) {
        logEnter("declarations");
        List decl = new ArrayList<>();
        if (ctx != null && ctx.declaration() != null) {
            for (CSSParser.DeclarationContext declctx : ctx.declaration()) {
                Declaration d = visitDeclaration(declctx);
                if (d != null) {
                    decl.add(d);
                    if (log.isDebugEnabled()) {
                        log.debug("Inserted declaration #{} ", decl.size() + 1);
                    }
                } else {
                    if (log.isDebugEnabled()) {
                        log.debug("Null declaration was omitted");
                    }
                }
            }
        }
        logLeave("declarations");
        return decl;
    }

    protected static class declaration_scope {
        cz.vutbr.web.css.Declaration d;
        boolean invalid;
    }

    protected Stack declaration_stack = new Stack<>();

    @Override
    /**
     *
     declaration : property COLON S* terms? important?
     | noprop any*
     */
    public Declaration visitDeclaration(CSSParser.DeclarationContext ctx) {
        logEnter("declaration");
        Declaration decl;
        declaration_stack.push(new declaration_scope());
        declaration_stack.peek().d = decl = rf.createDeclaration();
        declaration_stack.peek().invalid = false;

        if (ctx.noprop() == null && !ctxHasErrorNode(ctx)) {
            if (ctx.important() != null) {
                visitImportant(ctx.important());
            }
            visitProperty(ctx.property());
            if (ctx.terms() != null) {
                List> t = visitTerms(ctx.terms());
                decl.replaceAll(t);
            }
        } else {
            if (log.isDebugEnabled()) {
                log.debug("invalidating declaration");
            }
            declaration_stack.peek().invalid = true;
        }

        if (declaration_stack.peek().invalid || declaration_stack.isEmpty()) {
            decl = null;
            if (log.isDebugEnabled()) {
                log.debug("Declaration was invalidated or already invalid");
            }
        } else {
            if (log.isDebugEnabled()) {
                log.debug("Returning declaration: {}.", decl);
            }
        }
        logLeave("declaration");
        declaration_stack.pop();

        return decl;
    }

    @Override
    /**
     * important
     : EXCLAMATION S* IMPORTANT S*
     */
    public Object visitImportant(CSSParser.ImportantContext ctx) {
        if (ctxHasErrorNode(ctx)) {
            declaration_stack.peek().invalid = true;
        } else {
            declaration_stack.peek().d.setImportant(true);
            if (log.isDebugEnabled()) {
                log.debug("IMPORTANT");
            }
        }
        //returns null
        return null;
    }

    @Override
    /**
     * Setting property of declaration
     property
     : MINUS? IDENT S*

     returns null - processed via declaration_sctack
     */
    public Object visitProperty(CSSParser.PropertyContext ctx) {
        logEnter("property");

        String property = extractTextUnescaped(ctx.IDENT().getText());
        if (ctx.MINUS() != null) {
            property = ctx.MINUS().getText() + property;
        }
        declaration_stack.peek().d.setProperty(property);
        Token token = ctx.IDENT().getSymbol();
        declaration_stack.peek().d.setSource(extractSource((CSSToken) token));

        if (log.isDebugEnabled()) {
            log.debug("Setting property: {}", declaration_stack.peek().d.getProperty());
        }
        logLeave("property");
        //returns null
        return null;
    }


    protected static class terms_scope {
        List> list;
        cz.vutbr.web.css.Term term;
        cz.vutbr.web.css.Term.Operator op;
        int unary;
        boolean dash;
    }

    protected Stack terms_stack = new Stack<>();

    @Override
    /**
     * Term of CSSDeclaration
     *
     * terms : term+
     */
    public List> visitTerms(CSSParser.TermsContext ctx) {
        terms_stack.push(new terms_scope());
        List> tlist;
        logEnter("terms");
        terms_stack.peek().list = tlist = new ArrayList<>();
        terms_stack.peek().term = null;
        terms_stack.peek().op = null;
        terms_stack.peek().unary = 1;
        terms_stack.peek().dash = false;
        if (ctx.term() != null)
        {
            for (CSSParser.TermContext trmCtx : ctx.term()) {
                if (trmCtx instanceof CSSParser.TermValuePartContext) {
                    visitTermValuePart((CSSParser.TermValuePartContext) trmCtx);
                    // set operator, store and create next
                    if (!declaration_stack.peek().invalid && terms_stack.peek().term != null) {
                        terms_stack.peek().term.setOperator(terms_stack.peek().op);
                        terms_stack.peek().list.add(terms_stack.peek().term);
                        // reinitialization
                        terms_stack.peek().op = cz.vutbr.web.css.Term.Operator.SPACE;
                        terms_stack.peek().unary = 1;
                        terms_stack.peek().dash = false;
                        terms_stack.peek().term = null;
                    }
                } else {
                    visitTermInvalid((CSSParser.TermInvalidContext) trmCtx);
                }
            }
        }
        if (log.isDebugEnabled()) {
            log.debug("Totally added {} terms", tlist.size());
        }
        logLeave("terms");
        terms_stack.pop();
        return tlist;
    }

    /**
     * term
     * : valuepart #termValuePart
     * | LCURLY S* (any | SEMICOLON S*)* RCURLY #termInvalid // invalid term
     * | ATKEYWORD S* #termInvalid // invalid term
     * ;
     */
    //
    @Override
    public Object visitTermValuePart(CSSParser.TermValuePartContext ctx) {
        logEnter("term");
        visitValuepart(ctx.valuepart());
        //returns null
        return null;
    }

    @Override
    public Object visitTermInvalid(CSSParser.TermInvalidContext ctx) {
        logEnter("term");
        declaration_stack.peek().invalid = true;
        //returns null
        return null;
    }


    @Override
    public Object visitFunct(CSSParser.FunctContext ctx) {
        if (ctx.EXPRESSION() != null) {
            log.warn("Omitting expression " + ctx.getText() + ", expressions are not supported");
            return null;
        }
        Term ret = null;
        final String fname = extractTextUnescaped(ctx.FUNCTION().getText()).toLowerCase();
        if (ctx.funct_args() != null)
        {
            List> t = visitFunct_args(ctx.funct_args());
            if (fname.equals("url")) {
                // the function name is url() after escaping - create an URI
                if (t == null || t.size() != 1)
                    ret = null;
                else {
                    cz.vutbr.web.css.Term term = t.get(0);
                    if (term instanceof cz.vutbr.web.css.TermString && term.getOperator() == null)
                        ret = tf.createURI(((cz.vutbr.web.css.TermString) term).getValue(), extractBase(ctx.FUNCTION()));
                    else
                        ret = null;
                }
            } else if (fname.equals("calc")) {
                // create calc() of the given type: , , ,