private Evaluator byFunction(XTokenQueue predicatesQueue) { for (Map.Entry<String, FunctionEvaluator> entry : FUNCTION_MAPPING.entrySet()) { if (predicatesQueue.matchChomp(entry.getKey())) { String paramString = predicatesQueue.chompBalanced('(', ')'); List<String> params = XTokenQueue.trimQuotes(XTokenQueue.parseFuncionParams(paramString)); if (params.get(0).startsWith("@")) { params.set(0, params.get(0).substring(1)); return entry.getValue().call(params.toArray(new String[0])); } else { return null; } } } throw new Selector.SelectorParseException("Could not parse query '%s': unexpected token at '%s'", query, predicatesQueue.remainder()); }
public CacheCSSFunction() { cache = CacheBuilder.newBuilder().build(new CacheLoader<String, Evaluator>() { @Override public Evaluator load(String key) throws Exception { return QueryParser.parse(key); } }); }
/** * Parse the query * * @return Evaluator */ Evaluator parse() { tq.consumeWhitespace(); if (tq.matchesAny(combinators)) { // if starts with a combinator, use root as elements evals.add(new StructuralEvaluator.Root()); combinator(tq.consume()); } else { findElements(); } while (!tq.isEmpty()) { // hierarchy and extras boolean seenWhite = tq.consumeWhitespace(); if (tq.matchesAny(combinators)) { combinator(tq.consume()); } else if (seenWhite) { combinator(' '); } else { // E.class, E#id, E[attr] etc. AND findElements(); // take next el, #. etc off queue } } if (evals.size() == 1) return evals.get(0); return new CombiningEvaluator.And(evals); }
private void byTag() { String tagName = tq.consumeElementSelector(); Validate.notEmpty(tagName); // namespaces: if element name is "abc:def", selector must be "abc|def", so flip: if (tagName.contains("|")) tagName = tagName.replace("|", ":"); evals.add(new Evaluator.Tag(tagName.trim().toLowerCase())); }
private void byAttribute() { TokenQueue cq = new TokenQueue(tq.chompBalanced('[', ']')); // content queue String key = cq.consumeToAny(AttributeEvals); // eq, not, start, end, contain, match, (no val) Validate.notEmpty(key); cq.consumeWhitespace(); if (cq.isEmpty()) { if (key.startsWith("^")) evals.add(new Evaluator.AttributeStarting(key.substring(1))); else evals.add(new Evaluator.Attribute(key)); } else { if (cq.matchChomp("=")) evals.add(new Evaluator.AttributeWithValue(key, cq.remainder())); else if (cq.matchChomp("!=")) evals.add(new Evaluator.AttributeWithValueNot(key, cq.remainder())); else if (cq.matchChomp("^=")) evals.add(new Evaluator.AttributeWithValueStarting(key, cq.remainder())); else if (cq.matchChomp("$=")) evals.add(new Evaluator.AttributeWithValueEnding(key, cq.remainder())); else if (cq.matchChomp("*=")) evals.add(new Evaluator.AttributeWithValueContaining(key, cq.remainder())); else if (cq.matchChomp("~=")) evals.add(new Evaluator.AttributeWithValueMatching(key, Pattern.compile(cq.remainder()))); else throw new Selector.SelectorParseException( "Could not parse attribute query '%s': unexpected token at '%s'", query, cq.remainder()); } }
private void cssNthChild(boolean backwards, boolean ofType) { String argS = tq.chompTo(")").trim().toLowerCase(); Matcher mAB = NTH_AB.matcher(argS); Matcher mB = NTH_B.matcher(argS); final int a, b; if ("odd".equals(argS)) { a = 2; b = 1; } else if ("even".equals(argS)) { a = 2; b = 0; } else if (mAB.matches()) { a = mAB.group(3) != null ? Integer.parseInt(mAB.group(1).replaceFirst("^\\+", "")) : 1; b = mAB.group(4) != null ? Integer.parseInt(mAB.group(4).replaceFirst("^\\+", "")) : 0; } else if (mB.matches()) { a = 0; b = Integer.parseInt(mB.group().replaceFirst("^\\+", "")); } else { throw new Selector.SelectorParseException("Could not parse nth-index '%s': unexpected format", argS); } if (ofType) if (backwards) evals.add(new Evaluator.IsNthLastOfType(a, b)); else evals.add(new Evaluator.IsNthOfType(a, b)); else { if (backwards) evals.add(new Evaluator.IsNthLastChild(a, b)); else evals.add(new Evaluator.IsNthChild(a, b)); } }
private void contains(boolean own) { tq.consume(own ? ":containsOwn" : ":contains"); String searchText = TokenQueue.unescape(tq.chompBalanced('(', ')')); Validate.notEmpty(searchText, ":contains(text) query must not be empty"); if (own) evals.add(new Evaluator.ContainsOwnText(searchText)); else evals.add(new Evaluator.ContainsText(searchText)); }
private void matches(boolean own) { tq.consume(own ? ":matchesOwn" : ":matches"); String regex = tq.chompBalanced('(', ')'); // don't unescape, as regex bits will be escaped Validate.notEmpty(regex, ":matches(regex) query must not be empty"); if (own) evals.add(new Evaluator.MatchesOwn(Pattern.compile(regex))); else evals.add(new Evaluator.Matches(Pattern.compile(regex))); }
@Override public boolean matches(Element root, Element node) { for (int i = 0; i < num; i++) { Evaluator s = evaluators.get(i); if (!s.matches(root, node)) return false; } return true; }
/** * Create a new Or evaluator. The initial evaluators are ANDed together and used as the first clause of the * OR. * * @param evaluators initial OR clause (these are wrapped into an AND evaluator). */ Or(Collection<Evaluator> evaluators) { super(); if (num > 1) this.evaluators.add(new CombiningEvaluator.And(evaluators)); else // 0 or 1 this.evaluators.addAll(evaluators); updateNumEvaluators(); }
@Override public boolean matches(Element root, Element node) { for (int i = 0; i < num; i++) { Evaluator s = evaluators.get(i); if (s.matches(root, node)) return true; } return false; }
/** * Finds elements, including and recursively under this element, with the specified tag name. * @param tagName The tag name to search for (case insensitively). * @return a matching unmodifiable list of elements. Will be empty if this element and none of its children match. */ public Elements getElementsByTag(String tagName) { Validate.notEmpty(tagName); tagName = tagName.toLowerCase().trim(); return Collector.collect(new Evaluator.Tag(tagName), this); }
/** * Find an element by ID, including or under this element. * <p> * Note that this finds the first matching ID, starting with this element. If you search down from a different * starting point, it is possible to find a different element by ID. For unique element by ID within a Document, * use {@link Document#getElementById(String)} * @param id The ID to search for. * @return The first matching element by ID, starting with this element, or null if none found. */ public Element getElementById(String id) { Validate.notEmpty(id); Elements elements = Collector.collect(new Evaluator.Id(id), this); if (elements.size() > 0) return elements.get(0); else return null; }
/** * Find elements that have a named attribute set. Case insensitive. * * @param key name of the attribute, e.g. {@code href} * @return elements that have this attribute, empty if none */ public Elements getElementsByAttribute(String key) { Validate.notEmpty(key); key = key.trim().toLowerCase(); return Collector.collect(new Evaluator.Attribute(key), this); }
/** * Find elements that have an attribute name starting with the supplied prefix. Use {@code data-} to find elements * that have HTML5 datasets. * @param keyPrefix name prefix of the attribute e.g. {@code data-} * @return elements that have attribute names that start with with the prefix, empty if none. */ public Elements getElementsByAttributeStarting(String keyPrefix) { Validate.notEmpty(keyPrefix); keyPrefix = keyPrefix.trim().toLowerCase(); return Collector.collect(new Evaluator.AttributeStarting(keyPrefix), this); }
@Override public boolean matches(Element root, Element node) { for (int i = 0; i < evaluators.size(); i++) { Evaluator s = evaluators.get(i); if (!s.matches(root, node)) return false; } return true; }
@Override public boolean matches(Element root, Element node) { for (int i = 0; i < evaluators.size(); i++) { Evaluator s = evaluators.get(i); if (s.matches(root, node)) return true; } return false; }
public void calc(Evaluator evaluator, Operation operation) { if (size() == 0) { push(evaluator); } else { if (operation == Operation.AND) { evaluator = new CombiningEvaluator.And(pop(), evaluator); } else { mergeOr(); } push(evaluator); } }
public void mergeOr() { if (size() >= 2) { Evaluator pop1 = pop(); Evaluator pop2 = pop(); Evaluator tempEvaluator = new CombiningEvaluator.Or(pop2, pop1); push(tempEvaluator); } }
private Evaluator consumePredicates(String queue) { XTokenQueue predicatesQueue = new XTokenQueue(queue); EvaluatorStack evaluatorStack = new EvaluatorStack(); Operation currentOperation = null; predicatesQueue.consumeWhitespace(); while (!predicatesQueue.isEmpty()) { if (predicatesQueue.matchChomp("and")) { currentOperation = Operation.AND; } else if (predicatesQueue.matchChomp("or")) { currentOperation = Operation.OR; } else { if (currentOperation == null && evaluatorStack.size() > 0) { throw new IllegalArgumentException(String.format("Need AND/OR between two predicate! %s", predicatesQueue.remainder())); } Evaluator evaluator; if (predicatesQueue.matches("(")) { evaluator = consumePredicates(predicatesQueue.chompBalanced('(', ')')); } else if (predicatesQueue.matches("@")) { evaluator = byAttribute(predicatesQueue); } else if (predicatesQueue.matchesRegex("\\w+.*")) { evaluator = byFunction(predicatesQueue); } else { throw new Selector.SelectorParseException("Could not parse query '%s': unexpected token at '%s'", query, predicatesQueue.remainder()); } evaluatorStack.calc(evaluator, currentOperation); //consume operator currentOperation = null; } predicatesQueue.consumeWhitespace(); } evaluatorStack.mergeOr(); return evaluatorStack.peek(); }
/** * Finds elements, including and recursively under this element, with the specified tag name. * @param tagName The tag name to search for (case insensitively). * @return a matching unmodifiable list of elements. Will be empty if this element and none of its children match. */ public Elements getElementsByTag(String tagName) { Validate.notEmpty(tagName); tagName = normalize(tagName); return Collector.collect(new Evaluator.Tag(tagName), this); }
/** * Find elements that have a named attribute set. Case insensitive. * * @param key name of the attribute, e.g. {@code href} * @return elements that have this attribute, empty if none */ public Elements getElementsByAttribute(String key) { Validate.notEmpty(key); key = key.trim(); return Collector.collect(new Evaluator.Attribute(key), this); }
/** * Find elements that have an attribute name starting with the supplied prefix. Use {@code data-} to find elements * that have HTML5 datasets. * @param keyPrefix name prefix of the attribute e.g. {@code data-} * @return elements that have attribute names that start with with the prefix, empty if none. */ public Elements getElementsByAttributeStarting(String keyPrefix) { Validate.notEmpty(keyPrefix); keyPrefix = keyPrefix.trim(); return Collector.collect(new Evaluator.AttributeStarting(keyPrefix), this); }