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

dev.amp.validator.utils.SelectorUtils Maven / Gradle / Ivy

There is a newer version: 1.0.42
Show newest version
/*
 *
 * ====================================================================
 * 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.
 *  ====================================================================
 */

/*
 * Changes to the original project are Copyright 2019, Yahoo Inc..
 */

package dev.amp.validator.utils;

import com.steadystate.css.parser.Token;
import dev.amp.validator.ValidatorProtos;
import dev.amp.validator.css.CssTokenUtil;
import dev.amp.validator.css.CssValidationException;
import dev.amp.validator.css.EOFToken;
import dev.amp.validator.css.ErrorToken;
import dev.amp.validator.css.TokenStream;
import dev.amp.validator.css.TokenType;
import dev.amp.validator.selector.AttrSelector;
import dev.amp.validator.selector.ClassSelector;
import dev.amp.validator.selector.Combinator;
import dev.amp.validator.selector.IdSelector;
import dev.amp.validator.selector.PseudoSelector;
import dev.amp.validator.selector.Selector;
import dev.amp.validator.selector.SelectorException;
import dev.amp.validator.selector.SimpleSelectorSequence;
import dev.amp.validator.selector.TypeSelector;

import javax.annotation.Nonnull;

import java.util.ArrayList;
import java.util.List;

import static dev.amp.validator.css.Canonicalizer.consumeAFunction;
import static dev.amp.validator.css.CssTokenUtil.copyPosTo;
import static dev.amp.validator.css.CssTokenUtil.getTokenType;

/**
 * Methods to handle selector validation.
 *
 * @author nhant01
 * @author GeorgeLuo
 */

public final class SelectorUtils {
    /**
     * Private constructor.
     */
    private SelectorUtils() {
    }

    /**
     * Whether or not the provided token could be the start of a simple
     * selector sequence. See the simple_selector_sequence production in
     * http://www.w3.org/TR/css3-selectors/#grammar.
     *
     * @param token to analyze
     * @return is SimpleSelector Sequence Start
     */
    public static boolean isSimpleSelectorSequenceStart(@Nonnull final com.steadystate.css.parser.Token token) {
        // Type selector start.
        if (isDelim(token, "*") || isDelim(token, "|") || (getTokenType(token) == TokenType.IDENT)) {
            return true;
        }
        // Id selector start.
        if (getTokenType(token) == TokenType.HASH) {
            return true;
        }
        // Class selector start.
        if (isDelim(token, ".")) {
            return true;
        }
        // Attr selector start.
        if (getTokenType(token) == TokenType.OPEN_SQUARE) {
            return true;
        }
        // A pseudo selector.
        if (getTokenType(token) == TokenType.COLON) {
            return true;
        }
        // TODO(johannes): add the others.
        return false;
    }

    /**
     * Helper function for determining whether the provided token is a specific
     * delimiter.
     *
     * @param token     to analyze
     * @param delimChar to check
     * @return if this is a delim char
     */
    public static boolean isDelim(@Nonnull final com.steadystate.css.parser.Token token, @Nonnull final String delimChar) {
        return getTokenType(token) == TokenType.DELIM && (token.image.trim()).equals(delimChar);
    }

    /**
     * tokenStream.current must be the first token of the sequence.
     * This function will return an error if no selector is found.
     *
     * @param tokenStream token stream
     * @return the SimpleSelectorSequence instance
     * @throws CssValidationException the CssValidationException
     * @throws SelectorException the SelectorException
     */
    public static SimpleSelectorSequence parseASimpleSelectorSequence(@Nonnull final TokenStream tokenStream)
            throws SelectorException, CssValidationException {
        final Token start = tokenStream.current();
        TypeSelector typeSelector = null;
        if (isDelim(tokenStream.current(), "*")
                || isDelim(tokenStream.current(), "|")
                || getTokenType(tokenStream.current()) == TokenType.IDENT) {
            typeSelector = parseATypeSelector(tokenStream);
        }
        final List otherSelectors = new ArrayList<>();
        while (true) {
            if (getTokenType(tokenStream.current()) == TokenType.HASH) {
                otherSelectors.add(parseAnIdSelector(tokenStream));
            } else if (isDelim(tokenStream.current(), ".")
                    && getTokenType(tokenStream.next()) == TokenType.IDENT) {
                otherSelectors.add(parseAClassSelector(tokenStream));
            } else if (getTokenType(tokenStream.current()) == TokenType.OPEN_SQUARE) {
                AttrSelector maybeAttrSelector = parseAnAttrSelector(tokenStream);
                otherSelectors.add(maybeAttrSelector);
            } else if (getTokenType(tokenStream.current()) == TokenType.COLON) {
                PseudoSelector maybePseudo;
                try {
                    maybePseudo = parseAPseudoSelector(tokenStream);
                } catch (SelectorException selectorException) {
                    throw selectorException;
                }

                otherSelectors.add(maybePseudo);
                // NOTE: If adding more 'else if' clauses here, be sure to udpate
                // isSimpleSelectorSequenceStart accordingly.
            } else {
                if (typeSelector == null) {
                    if (otherSelectors.size() == 0) {
                        final List params = new ArrayList<>();
                        params.add("style");
                        final ErrorToken errorToken = new ErrorToken(
                                ValidatorProtos.ValidationError.Code.CSS_SYNTAX_MISSING_SELECTOR,
                                params);
                        throw new SelectorException((ErrorToken) copyPosTo(tokenStream.current(), errorToken));
                    }
                    // If no type selector is given then the universal selector is
                    // implied.
                    typeSelector = (TypeSelector) copyPosTo(start, new TypeSelector(
                            /*namespacePrefix=*/ null, /*elementName=*/ "*"));
                }
                return (SimpleSelectorSequence) copyPosTo(start, new SimpleSelectorSequence(typeSelector, otherSelectors));
            }
        }
    }

    /**
     * tokenStream.current() must be the ColonToken. Returns an error if
     * the pseudo token can't be parsed (e.g., a lone ':').
     *
     * @param tokenStream token stream
     * @return the pseudo selector
     * @throws CssValidationException the CssValidationException
     * @throws SelectorException the SelectorException
     */
    private static PseudoSelector parseAPseudoSelector(@Nonnull final TokenStream tokenStream)
            throws SelectorException, CssValidationException {
        if (getTokenType(tokenStream.current()) != TokenType.COLON) {
            throw new SelectorException("Precondition violated: must be a \":\"");
        }
        final Token firstColon = tokenStream.current();
        tokenStream.consume();
        boolean isClass = true;
        if (getTokenType(tokenStream.current()) == TokenType.COLON) {
            // '::' starts a pseudo element, ':' starts a pseudo class.
            isClass = false;
            tokenStream.consume();
        }
        if (getTokenType(tokenStream.current()) == TokenType.IDENT) {
            final Token ident = tokenStream.current();
            String name = ident.image;
            tokenStream.consume();
            return (PseudoSelector) copyPosTo(firstColon, new PseudoSelector(isClass, name, new ArrayList<>()));
        } else if (getTokenType(tokenStream.current()) == TokenType.FUNCTION_TOKEN) {
            Token funcToken = tokenStream.current();
            List errors = new ArrayList<>();
            final List func = extractAFunction(tokenStream, errors);
            if (errors.size() > 0) {
                throw new SelectorException(errors.get(0));
            }
            tokenStream.consume();
            return (PseudoSelector) copyPosTo(firstColon, new PseudoSelector(isClass, funcToken.image, func));
        } else {
            final List params = new ArrayList<>();
            params.add("style");
            final ErrorToken errorToken = new ErrorToken(
                    ValidatorProtos.ValidationError.Code.CSS_SYNTAX_ERROR_IN_PSEUDO_SELECTOR,
                    params);
            throw new SelectorException((ErrorToken) copyPosTo(tokenStream.current(), errorToken));
        }
    }

    /**
     * The selector production from
     * http://www.w3.org/TR/css3-selectors/#grammar
     * Returns an ErrorToken if no selector is found.
     *
     * @param tokenStream token stream
     * @return the selector
     * @throws CssValidationException the CssValidationException
     * @throws SelectorException the SelectorException
     */
    public static Selector parseASelector(@Nonnull final TokenStream tokenStream)
            throws CssValidationException, SelectorException {
        if (!isSimpleSelectorSequenceStart(tokenStream.current())) {
            final List params = new ArrayList<>();
            params.add("style");
            final ErrorToken errorToken = new ErrorToken(
                    ValidatorProtos.ValidationError.Code.CSS_SYNTAX_NOT_A_SELECTOR_START,
                    params);
            throw new SelectorException((ErrorToken) copyPosTo(tokenStream.current(), errorToken));
        }

        SimpleSelectorSequence parsed;

        try {
            parsed = parseASimpleSelectorSequence(tokenStream);
        } catch (final SelectorException selectorException) {
            throw selectorException;
        } catch (final CssValidationException cssValidationException) {
            throw cssValidationException;
        }

        Selector left = parsed;
        while (true) {
            // Consume whitespace in front of combinators, while being careful
            // to not eat away the infamous "whitespace operator" (sigh, haha).
            if ((getTokenType(tokenStream.current()) == TokenType.WHITESPACE && !isSimpleSelectorSequenceStart(tokenStream.next()))) {
                tokenStream.consume();
            }
            // If present, grab the combinator token which we'll use for line
            // / column info.
            if (!(((getTokenType(tokenStream.current()) == TokenType.WHITESPACE && isSimpleSelectorSequenceStart(tokenStream.next()))
                    || isDelim(tokenStream.current(), "+")
                    || isDelim(tokenStream.current(), ">")
                    || isDelim(tokenStream.current(), "~")))) {
                return left;
            }
            final Token combinatorToken = tokenStream.current();
            tokenStream.consume();
            if (getTokenType(tokenStream.current()) == TokenType.WHITESPACE) {
                tokenStream.consume();
            }

            final SimpleSelectorSequence right = parseASimpleSelectorSequence(tokenStream);

            left = (Selector) copyPosTo(combinatorToken, new Combinator(
                    combinatorTypeForToken(combinatorToken), left, right));
        }
    }

    /**
     * The CombinatorType for a given token; helper function used when
     * constructing a Combinator instance.
     *
     * @param token the token
     * @return the combinator type
     * @throws CssValidationException the CssValidationException
     */
    public static Combinator.CombinatorType combinatorTypeForToken(@Nonnull final Token token) throws CssValidationException {
        if (getTokenType(token) == TokenType.WHITESPACE) {
            return Combinator.CombinatorType.DESCENDANT;
        } else if (isDelim(token, ">")) {
            return Combinator.CombinatorType.CHILD;
        } else if (isDelim(token, "+")) {
            return Combinator.CombinatorType.ADJACENT_SIBLING;
        } else if (isDelim(token, "~")) {
            return Combinator.CombinatorType.GENERAL_SIBLING;
        }

        throw new CssValidationException("'Internal Error: not a combinator token");
    }

    /**
     * tokenStream.current() is the first token of the type selector.
     *
     * @param tokenStream token stream
     * @return the type selector
     */
    public static TypeSelector parseATypeSelector(@Nonnull final TokenStream tokenStream) {
        String namespacePrefix = null;
        String elementName = "*";
        Token start = tokenStream.current();

        if (isDelim(tokenStream.current(), "|")) {
            namespacePrefix = "";
            tokenStream.consume();
        } else if (isDelim(tokenStream.current(), "*") && isDelim(tokenStream.next(), "|")) {
            namespacePrefix = "*";
            tokenStream.consume();
            tokenStream.consume();
        } else if (getTokenType(tokenStream.current()) == TokenType.IDENT && isDelim(tokenStream.next(), "|")) {
            Token ident = tokenStream.current();
            namespacePrefix = ident.image;
            tokenStream.consume();
            tokenStream.consume();
        }
        if (isDelim(tokenStream.current(), "*")) {
            elementName = "*";
            tokenStream.consume();
        } else if (getTokenType(tokenStream.current()) == TokenType.IDENT) {
            Token ident = tokenStream.current();
            elementName = ident.image;
            tokenStream.consume();
        }
        return (TypeSelector) copyPosTo(start, new TypeSelector(namespacePrefix, elementName));
    }

    /**
     * tokenStream.current() must be the hash token.
     *
     * @param tokenStream token stream
     * @return the selector
     * @throws SelectorException the SelectorException
     */
    private static Selector parseAnIdSelector(@Nonnull final TokenStream tokenStream) throws SelectorException {
        if (getTokenType(tokenStream.current()) != TokenType.HASH) {
            throw new SelectorException("Precondition violated: must start with HashToken");
        }

        Token hash = tokenStream.current();
        tokenStream.consume();
        return (Selector) copyPosTo(hash, new IdSelector(hash.image));
    }

    /**
     * tokenStream.current() must be the '.' delimiter token.
     *
     * @param tokenStream token stream
     * @return the class selector
     * @throws SelectorException the SelectorException
     */
    private static ClassSelector parseAClassSelector(@Nonnull final TokenStream tokenStream) throws
            SelectorException {
        if (getTokenType(tokenStream.next()) != TokenType.IDENT || !isDelim(tokenStream.current(), ".")) {
            throw new SelectorException("Precondition violated: must start with \".\" and follow with ident");
        }

        Token dot = tokenStream.current();
        tokenStream.consume();
        Token ident = tokenStream.current();
        tokenStream.consume();
        return (ClassSelector) copyPosTo(dot, new ClassSelector(ident.image));
    }

    /**
     * tokenStream.current() must be the open square token.
     *
     * @param tokenStream token stream
     * @return attribute selector
     * @throws CssValidationException the CssValidationException
     * @throws SelectorException the SelectorException
     */
    public static AttrSelector parseAnAttrSelector(@Nonnull final TokenStream tokenStream) throws
            SelectorException, CssValidationException {
        if (getTokenType(tokenStream.current()) != TokenType.OPEN_SQUARE) {
            throw new SelectorException("Precondition violated: must be an OpenSquareToken");
        }

        Token start = tokenStream.current();

        tokenStream.consume();  // Consumes '['.
        if (getTokenType(tokenStream.current()) == TokenType.WHITESPACE) {
            tokenStream.consume();
        }

        // This part is defined in https://www.w3.org/TR/css3-selectors/#attrnmsp:
        // Attribute selectors and namespaces. It is similar to parseATypeSelector.
        String namespacePrefix = null;
        if (isDelim(tokenStream.current(), "|")) {
            namespacePrefix = "";
            tokenStream.consume();
        } else if (isDelim(tokenStream.current(), "*") && isDelim(tokenStream.next(), "|")) {
            namespacePrefix = "*";
            tokenStream.consume();
            tokenStream.consume();
        } else if (getTokenType(tokenStream.current()) == TokenType.IDENT && isDelim(tokenStream.next(), "|")) {
            final Token ident = tokenStream.current();
            namespacePrefix = ident.image;
            tokenStream.consume();
            tokenStream.consume();
        }
        // Now parse the attribute name. This part is mandatory.
        if (getTokenType(tokenStream.current()) != TokenType.IDENT) {
            final List params = new ArrayList<>();
            params.add("style");
            final ErrorToken errorToken = new ErrorToken(
                    ValidatorProtos.ValidationError.Code.CSS_SYNTAX_INVALID_ATTR_SELECTOR,
                    params);
            CssTokenUtil.copyPosTo(tokenStream.current(), errorToken);
            throw new SelectorException(errorToken);
        }
        Token ident = tokenStream.current();
        String attrName = ident.image;
        tokenStream.consume();
        if (getTokenType(tokenStream.current()) == TokenType.WHITESPACE) {
            tokenStream.consume();
        }

        // After the attribute name, we may see an operator; if we do, then
        // we must see either a string or an identifier. This covers
        // 6.3.1 Attribute presence and value selectors
        // (https://www.w3.org/TR/css3-selectors/#attribute-representation) and
        // 6.3.2 Substring matching attribute selectors
        // (https://www.w3.org/TR/css3-selectors/#attribute-substrings).

        /** @type {string} */
        String matchOperator = "";
        TokenType current = getTokenType(tokenStream.current());
        if (isDelim(tokenStream.current(), "=")) {
            matchOperator = "=";
            tokenStream.consume();
        } else if (current == TokenType.INCLUDE_MATCH) {
            matchOperator = "~=";
            tokenStream.consume();
        } else if (current == TokenType.DASH_MATCH) {
            matchOperator = "|=";
            tokenStream.consume();
        } else if (current == TokenType.PREFIX_MATCH) {
            matchOperator = "^=";
            tokenStream.consume();
        } else if (current == TokenType.SUFFIX_MATCH) {
            matchOperator = "$=";
            tokenStream.consume();
        } else if (current == TokenType.SUBSTRING_MATCH) {
            matchOperator = "*=";
            tokenStream.consume();
        }
        if (getTokenType(tokenStream.current()) == TokenType.WHITESPACE) {
            tokenStream.consume();
        }

        String value = "";
        if (!matchOperator.equals("")) {  // If we saw an operator, parse the value.
            current = getTokenType(tokenStream.current());
            if (current == TokenType.IDENT) {
                ident = tokenStream.current();
                value = ident.image;
                tokenStream.consume();
            } else if (current == TokenType.STRING) {
                value = tokenStream.current().image;
                tokenStream.consume();
            }
        }
        if (getTokenType(tokenStream.current()) == TokenType.WHITESPACE) {
            tokenStream.consume();
        }
        // The attribute selector must in any case terminate with a close square
        // token.
        if (getTokenType(tokenStream.current()) != TokenType.CLOSE_SQUARE) {
            final List params = new ArrayList<>();
            params.add("style");
            final ErrorToken errorToken = new ErrorToken(
                    ValidatorProtos.ValidationError.Code.CSS_SYNTAX_INVALID_ATTR_SELECTOR,
                    params);
            CssTokenUtil.copyPosTo(tokenStream.current(), errorToken);
            throw new SelectorException(errorToken);
        }
        tokenStream.consume();
        final AttrSelector selector =
                new AttrSelector(namespacePrefix, attrName, matchOperator, value);
        return (AttrSelector) copyPosTo(start, selector);
    }

    /**
     * Returns a function's contents in tokenList, including the leading
     * FunctionToken, but excluding the trailing CloseParen token and
     * appended with an EOFToken instead.
     *
     * @param tokenStream token stream
     * @param errors array of error tokens
     * @return the list of token
     * @throws CssValidationException the CssValidationException
     */
    public static List extractAFunction(@Nonnull final TokenStream tokenStream,
                                               @Nonnull final List errors) throws CssValidationException {
        final List consumedTokens = new ArrayList<>();
        if (!consumeAFunction(tokenStream, consumedTokens, /*depth*/ 0)) {
            final List params = new ArrayList<>();
            params.add("style");
            final ErrorToken errorToken = new ErrorToken(
                    ValidatorProtos.ValidationError.Code.CSS_EXCESSIVELY_NESTED,
                    params);
            CssTokenUtil.copyPosTo(tokenStream.current(), errorToken);
            errors.add(errorToken);
        }
        // A function always has a start FunctionToken and
        // either a CloseParenToken or EOFToken.
        if (consumedTokens.size() < 2) {
            throw new CssValidationException("Internal Error: extractAFunction precondition not met");
        }

        // Convert end token to EOF.
        int end = consumedTokens.size() - 1;
        consumedTokens.set(end, copyPosTo(consumedTokens.get(end), new EOFToken()));
        return consumedTokens;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy