com.itextpdf.styledxmlparser.jsoup.select.Evaluator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of styled-xml-parser Show documentation
Show all versions of styled-xml-parser Show documentation
Styled XML parser is used by iText modules to parse HTML and XML
/*
This file is part of the iText (R) project.
Copyright (c) 1998-2020 iText Group NV
Authors: iText Software.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License version 3
as published by the Free Software Foundation with the addition of the
following permission added to Section 15 as permitted in Section 7(a):
FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
OF THIRD PARTY RIGHTS
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program; if not, see http://www.gnu.org/licenses or write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA, 02110-1301 USA, or download the license from the following URL:
http://itextpdf.com/terms-of-use/
The interactive user interfaces in modified source and object code versions
of this program must display Appropriate Legal Notices, as required under
Section 5 of the GNU Affero General Public License.
In accordance with Section 7(b) of the GNU Affero General Public License,
a covered work must retain the producer line in every PDF that is created
or manipulated using iText.
You can be released from the requirements of the license by purchasing
a commercial license. Buying such a license is mandatory as soon as you
develop commercial activities involving the iText software without
disclosing the source code of your own applications.
These activities include: offering paid services to customers as an ASP,
serving PDFs on the fly in a web application, shipping iText with a closed
source product.
For more information, please contact iText Software Corp. at this
address: [email protected]
*/
package com.itextpdf.styledxmlparser.jsoup.select;
import com.itextpdf.styledxmlparser.jsoup.PortUtil;
import com.itextpdf.styledxmlparser.jsoup.helper.Validate;
import com.itextpdf.styledxmlparser.jsoup.nodes.DocumentType;
import com.itextpdf.styledxmlparser.jsoup.nodes.XmlDeclaration;
import com.itextpdf.styledxmlparser.jsoup.nodes.Comment;
import com.itextpdf.styledxmlparser.jsoup.nodes.Document;
import com.itextpdf.styledxmlparser.jsoup.nodes.Element;
import com.itextpdf.styledxmlparser.jsoup.nodes.Node;
import com.itextpdf.io.util.MessageFormatUtil;
import java.util.List;
import java.util.regex.Pattern;
/**
* Evaluates that an element matches the selector.
*/
public abstract class Evaluator {
protected Evaluator() {
}
/**
* Test if the element meets the evaluator's requirements.
*
* @param root Root of the matching subtree
* @param element tested element
* @return Returns true if the requirements are met or
* false otherwise
*/
public abstract boolean matches(Element root, Element element);
/**
* Evaluator for tag name
*/
public static final class Tag extends Evaluator {
private String tagName;
public Tag(String tagName) {
this.tagName = tagName;
}
@Override
public boolean matches(Element root, Element element) {
return (element.tagName().equals(tagName));
}
@Override
public String toString() {
return MessageFormatUtil.format("{0}", tagName);
}
}
/**
* Evaluator for element id
*/
public static final class Id extends Evaluator {
private String id;
public Id(String id) {
this.id = id;
}
@Override
public boolean matches(Element root, Element element) {
return (id.equals(element.id()));
}
@Override
public String toString() {
return MessageFormatUtil.format("#{0}", id);
}
}
/**
* Evaluator for element class
*/
public static final class Class extends Evaluator {
private String className;
public Class(String className) {
this.className = className;
}
@Override
public boolean matches(Element root, Element element) {
return (element.hasClass(className));
}
@Override
public String toString() {
return MessageFormatUtil.format(".{0}", className);
}
}
/**
* Evaluator for attribute name matching
*/
public static final class Attribute extends Evaluator {
private String key;
public Attribute(String key) {
this.key = key;
}
@Override
public boolean matches(Element root, Element element) {
return element.hasAttr(key);
}
@Override
public String toString() {
return MessageFormatUtil.format("[{0}]", key);
}
}
/**
* Evaluator for attribute name prefix matching
*/
public static final class AttributeStarting extends Evaluator {
private String keyPrefix;
public AttributeStarting(String keyPrefix) {
this.keyPrefix = keyPrefix;
}
@Override
public boolean matches(Element root, Element element) {
List values = element.attributes().asList();
for (com.itextpdf.styledxmlparser.jsoup.nodes.Attribute attribute : values) {
if (attribute.getKey().startsWith(keyPrefix))
return true;
}
return false;
}
@Override
public String toString() {
return MessageFormatUtil.format("[^{0}]", keyPrefix);
}
}
/**
* Evaluator for attribute name/value matching
*/
public static final class AttributeWithValue extends AttributeKeyPair {
public AttributeWithValue(String key, String value) {
super(key, value);
}
@Override
public boolean matches(Element root, Element element) {
return element.hasAttr(key) && value.equalsIgnoreCase(element.attr(key).trim());
}
@Override
public String toString() {
return MessageFormatUtil.format("[{0}={1}]", key, value);
}
}
/**
* Evaluator for attribute name != value matching
*/
public static final class AttributeWithValueNot extends AttributeKeyPair {
public AttributeWithValueNot(String key, String value) {
super(key, value);
}
@Override
public boolean matches(Element root, Element element) {
return !value.equalsIgnoreCase(element.attr(key));
}
@Override
public String toString() {
return MessageFormatUtil.format("[{0}!={1}]", key, value);
}
}
/**
* Evaluator for attribute name/value matching (value prefix)
*/
public static final class AttributeWithValueStarting extends AttributeKeyPair {
public AttributeWithValueStarting(String key, String value) {
super(key, value);
}
@Override
public boolean matches(Element root, Element element) {
return element.hasAttr(key) && element.attr(key).toLowerCase().startsWith(value); // value is lower case already
}
@Override
public String toString() {
return MessageFormatUtil.format("[{0}^={1}]", key, value);
}
}
/**
* Evaluator for attribute name/value matching (value ending)
*/
public static final class AttributeWithValueEnding extends AttributeKeyPair {
public AttributeWithValueEnding(String key, String value) {
super(key, value);
}
@Override
public boolean matches(Element root, Element element) {
return element.hasAttr(key) && element.attr(key).toLowerCase().endsWith(value); // value is lower case
}
@Override
public String toString() {
return MessageFormatUtil.format("[{0}$={1}]", key, value);
}
}
/**
* Evaluator for attribute name/value matching (value containing)
*/
public static final class AttributeWithValueContaining extends AttributeKeyPair {
public AttributeWithValueContaining(String key, String value) {
super(key, value);
}
@Override
public boolean matches(Element root, Element element) {
return element.hasAttr(key) && element.attr(key).toLowerCase().contains(value); // value is lower case
}
@Override
public String toString() {
return MessageFormatUtil.format("[{0}*={1}]", key, value);
}
}
/**
* Evaluator for attribute name/value matching (value regex matching)
*/
public static final class AttributeWithValueMatching extends Evaluator {
String key;
Pattern pattern;
public AttributeWithValueMatching(String key, Pattern pattern) {
this.key = key.trim().toLowerCase();
this.pattern = pattern;
}
@Override
public boolean matches(Element root, Element element) {
return element.hasAttr(key) && pattern.matcher(element.attr(key)).find();
}
@Override
public String toString() {
return MessageFormatUtil.format("[{0}~={1}]", key, pattern.toString());
}
}
/**
* Abstract evaluator for attribute name/value matching
*/
public abstract static class AttributeKeyPair extends Evaluator {
String key;
String value;
public AttributeKeyPair(String key, String value) {
Validate.notEmpty(key);
Validate.notEmpty(value);
this.key = key.trim().toLowerCase();
if (value.startsWith("\"") && value.endsWith("\"")
|| value.startsWith("'") && value.endsWith("'")) {
value = value.substring(1, value.length()-1);
}
this.value = value.trim().toLowerCase();
}
}
/**
* Evaluator for any / all element matching
*/
public static final class AllElements extends Evaluator {
@Override
public boolean matches(Element root, Element element) {
return true;
}
@Override
public String toString() {
return "*";
}
}
/**
* Evaluator for matching by sibling index number (e {@literal <} idx)
*/
public static final class IndexLessThan extends IndexEvaluator {
public IndexLessThan(int index) {
super(index);
}
@Override
public boolean matches(Element root, Element element) {
return element.elementSiblingIndex() < index;
}
@Override
public String toString() {
return MessageFormatUtil.format(":lt({0})", index);
}
}
/**
* Evaluator for matching by sibling index number (e {@literal >} idx)
*/
public static final class IndexGreaterThan extends IndexEvaluator {
public IndexGreaterThan(int index) {
super(index);
}
@Override
public boolean matches(Element root, Element element) {
return element.elementSiblingIndex() > index;
}
@Override
public String toString() {
return MessageFormatUtil.format(":gt({0})", index);
}
}
/**
* Evaluator for matching by sibling index number (e = idx)
*/
public static final class IndexEquals extends IndexEvaluator {
public IndexEquals(int index) {
super(index);
}
@Override
public boolean matches(Element root, Element element) {
return element.elementSiblingIndex() == index;
}
@Override
public String toString() {
return MessageFormatUtil.format(":eq({0})", index);
}
}
/**
* Evaluator for matching the last sibling (css :last-child)
*/
public static final class IsLastChild extends Evaluator {
@Override
public boolean matches(Element root, Element element) {
final Element p = (Element) element.parent();
return p != null && !(p instanceof Document) && element.elementSiblingIndex() == p.children().size()-1;
}
@Override
public String toString() {
return ":last-child";
}
}
public static final class IsFirstOfType extends IsNthOfType {
public IsFirstOfType() {
super(0,1);
}
@Override
public String toString() {
return ":first-of-type";
}
}
public static final class IsLastOfType extends IsNthLastOfType {
public IsLastOfType() {
super(0,1);
}
@Override
public String toString() {
return ":last-of-type";
}
}
public static abstract class CssNthEvaluator extends Evaluator {
protected final int a, b;
public CssNthEvaluator(int a, int b) {
this.a = a;
this.b = b;
}
public CssNthEvaluator(int b) {
this(0,b);
}
@Override
public boolean matches(Element root, Element element) {
final Element p = (Element) element.parent();
if (p == null || (p instanceof Document)) return false;
final int pos = calculatePosition(root, element);
if (a == 0) return pos == b;
return (pos-b)*a >= 0 && (pos-b)%a==0;
}
@Override
public String toString() {
if (a == 0)
return MessageFormatUtil.format(":{0}({1})",getPseudoClass(), b);
if (b == 0)
return MessageFormatUtil.format(":{0}({1}n)",getPseudoClass(), a);
return MessageFormatUtil.format(":{0}({1}n{2" + PortUtil.signedNumberFormat +"})", getPseudoClass(),a, b);
}
protected abstract String getPseudoClass();
protected abstract int calculatePosition(Element root, Element element);
}
/**
* css-compatible Evaluator for :eq (css :nth-child)
*
* @see IndexEquals
*/
public static final class IsNthChild extends CssNthEvaluator {
public IsNthChild(int a, int b) {
super(a,b);
}
protected int calculatePosition(Element root, Element element) {
return element.elementSiblingIndex()+1;
}
protected String getPseudoClass() {
return "nth-child";
}
}
/**
* css pseudo class :nth-last-child)
*
* @see IndexEquals
*/
public static final class IsNthLastChild extends CssNthEvaluator {
public IsNthLastChild(int a, int b) {
super(a,b);
}
@Override
protected int calculatePosition(Element root, Element element) {
return ((Element) element.parent()).children().size() - element.elementSiblingIndex();
}
@Override
protected String getPseudoClass() {
return "nth-last-child";
}
}
/**
* css pseudo class nth-of-type
*
*/
public static class IsNthOfType extends CssNthEvaluator {
public IsNthOfType(int a, int b) {
super(a,b);
}
protected int calculatePosition(Element root, Element element) {
int pos = 0;
Elements family = ((Element) element.parent()).children();
for (Element el : family) {
if (el.tag().equals(element.tag())) pos++;
if (el == element) break;
}
return pos;
}
@Override
protected String getPseudoClass() {
return "nth-of-type";
}
}
public static class IsNthLastOfType extends CssNthEvaluator {
public IsNthLastOfType(int a, int b) {
super(a, b);
}
@Override
protected int calculatePosition(Element root, Element element) {
int pos = 0;
Elements family = ((Element) element.parent()).children();
for (int i = element.elementSiblingIndex(); i < family.size(); i++) {
if (family.get(i).tag().equals(element.tag())) pos++;
}
return pos;
}
@Override
protected String getPseudoClass() {
return "nth-last-of-type";
}
}
/**
* Evaluator for matching the first sibling (css :first-child)
*/
public static final class IsFirstChild extends Evaluator {
@Override
public boolean matches(Element root, Element element) {
final Element p = (Element) element.parent();
return p != null && !(p instanceof Document) && element.elementSiblingIndex() == 0;
}
@Override
public String toString() {
return ":first-child";
}
}
/**
* css3 pseudo-class :root
* @see :root selector
*/
public static final class IsRoot extends Evaluator {
@Override
public boolean matches(Element root, Element element) {
final Element r = root instanceof Document?root.child(0):root;
return element == r;
}
@Override
public String toString() {
return ":root";
}
}
public static final class IsOnlyChild extends Evaluator {
@Override
public boolean matches(Element root, Element element) {
final Element p = (Element) element.parent();
return p!=null && !(p instanceof Document) && element.siblingElements().size() == 0;
}
@Override
public String toString() {
return ":only-child";
}
}
public static final class IsOnlyOfType extends Evaluator {
@Override
public boolean matches(Element root, Element element) {
final Element p = (Element) element.parent();
if (p==null || p instanceof Document) return false;
int pos = 0;
Elements family = p.children();
for (Element el : family) {
if (el.tag().equals(element.tag())) pos++;
}
return pos == 1;
}
@Override
public String toString() {
return ":only-of-type";
}
}
public static final class IsEmpty extends Evaluator {
@Override
public boolean matches(Element root, Element element) {
List family = element.childNodes();
for (Node n : family) {
if (!(n instanceof Comment || n instanceof XmlDeclaration || n instanceof DocumentType )) return false;
}
return true;
}
@Override
public String toString() {
return ":empty";
}
}
/**
* Abstract evaluator for sibling index matching
*
* @author ant
*/
public abstract static class IndexEvaluator extends Evaluator {
int index;
public IndexEvaluator(int index) {
this.index = index;
}
}
/**
* Evaluator for matching Element (and its descendants) text
*/
public static final class ContainsText extends Evaluator {
private String searchText;
public ContainsText(String searchText) {
this.searchText = searchText.toLowerCase();
}
@Override
public boolean matches(Element root, Element element) {
return (element.text().toLowerCase().contains(searchText));
}
@Override
public String toString() {
return MessageFormatUtil.format(":contains({0}", searchText);
}
}
/**
* Evaluator for matching Element's own text
*/
public static final class ContainsOwnText extends Evaluator {
private String searchText;
public ContainsOwnText(String searchText) {
this.searchText = searchText.toLowerCase();
}
@Override
public boolean matches(Element root, Element element) {
return (element.ownText().toLowerCase().contains(searchText));
}
@Override
public String toString() {
return MessageFormatUtil.format(":containsOwn({0}", searchText);
}
}
/**
* Evaluator for matching Element (and its descendants) text with regex
*/
public static final class Matches extends Evaluator {
private Pattern pattern;
public Matches(Pattern pattern) {
this.pattern = pattern;
}
@Override
public boolean matches(Element root, Element element) {
return pattern.matcher(element.text()).find();
}
@Override
public String toString() {
return MessageFormatUtil.format(":matches({0}", pattern);
}
}
/**
* Evaluator for matching Element's own text with regex
*/
public static final class MatchesOwn extends Evaluator {
private Pattern pattern;
public MatchesOwn(Pattern pattern) {
this.pattern = pattern;
}
@Override
public boolean matches(Element root, Element element) {
return pattern.matcher(element.ownText()).find();
}
@Override
public String toString() {
return MessageFormatUtil.format(":matchesOwn({0}", pattern);
}
}
}