From 1fb078dc04952b5af0118c28e670488a374fc1f0 Mon Sep 17 00:00:00 2001 From: Sylvain Jermini Date: Tue, 21 Jul 2026 00:50:45 +0200 Subject: [PATCH] refactor selector, remove base selector --- .../jfiveparse/BaseSelector.java | 457 ----------------- .../ch/digitalfondue/jfiveparse/Selector.java | 464 +++++++++++++++++- .../ch/digitalfondue/jfiveparse/W3CDom.java | 40 +- 3 files changed, 457 insertions(+), 504 deletions(-) delete mode 100644 src/main/java/ch/digitalfondue/jfiveparse/BaseSelector.java diff --git a/src/main/java/ch/digitalfondue/jfiveparse/BaseSelector.java b/src/main/java/ch/digitalfondue/jfiveparse/BaseSelector.java deleted file mode 100644 index 902b998..0000000 --- a/src/main/java/ch/digitalfondue/jfiveparse/BaseSelector.java +++ /dev/null @@ -1,457 +0,0 @@ -package ch.digitalfondue.jfiveparse; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.BiPredicate; -import java.util.function.Function; -import java.util.function.IntPredicate; -import java.util.function.Supplier; - -abstract class BaseSelector> { - - - private List, SelectableNode>> matchers = new ArrayList<>(); - private final Function> wrapper; - private final Function, T> unwrapper; - private final Map nameSpaceAlias; - - BaseSelector(Function> wrapper, Function, T> unwrapper, Map nameSpaceAlias) { - this.wrapper = wrapper; - this.unwrapper = unwrapper; - this.nameSpaceAlias = nameSpaceAlias; - } - - private void contains(String value) { - matchers.add((node, base) -> { - if (node instanceof SelectableNode.SelectableElement e) { - var textContent = e.getTextContent(); - return textContent != null && textContent.contains(value); - } - return false; - }); - } - - public R element(String name) { - matchers.add((node, base) -> isElement(node) && name.equals(node.getNodeName())); - return inst(); - } - - public R element(String name, String namespace) { - String namespaceToMatch = nameSpaceAlias.getOrDefault(namespace, namespace); - matchers.add((node, base) -> node instanceof SelectableNode.SelectableElement ce && name.equals(ce.getNodeName()) - && Objects.equals(namespaceToMatch, ce.getNamespaceURI())); - return inst(); - } - - public R hasClass(String value) { - attrValInList("class", value); - return inst(); - } - - public R hasClass(String value, String... others) { - hasClass(value); - if (others != null) { - for (String n : others) { - hasClass(n); - } - } - return inst(); - } - - public R id(String value) { - attrValEq("id", value); - return inst(); - } - - public R attr(String name) { - matchers.add(matchAttr(name, (attrValue, elem) -> true)); - return inst(); - } - - public R attrValEq(String name, String value) { - matchers.add(matchAttr(name, (attrValue, elem) -> Objects.equals(attrValue, value))); - return inst(); - } - - public R attrValInList(String name, String value) { - matchers.add(matchAttr(name, (attrValue, elem) -> DOMTokenList.extractValues(elem, name).contains(value))); - return inst(); - } - - public R attrValStartWith(String name, String value) { - matchers.add(matchAttr(name, (attrValue, elem) -> attrValue != null && value != null && attrValue.startsWith(value))); - return inst(); - } - - public R attrValEndWith(String name, String value) { - matchers.add(matchAttr(name, (attrValue, elem) -> attrValue != null && value != null && attrValue.endsWith(value))); - return inst(); - } - - public R attrValContains(String name, String value) { - matchers.add(matchAttr(name, (attrValue, elem) -> attrValue != null && value != null && attrValue.contains(value))); - return inst(); - } - - public R withDescendant() { - var ancestorMatcher = collectMatchers(); - matchers.add(withDescendant(ancestorMatcher)); - return inst(); - } - - public R withChild() { - var rules = collectMatchers(); - matchers.add((node, base) -> node.getParentNode() != null && rules.test(wrapper.apply(node.getParentNode()), base)); - return inst(); - } - - private void nextSibling() { - var rules = collectMatchers(); - matchers.add((node, base) -> { - var previousElementSibling = wrapper.apply(node.getPreviousElementSibling()); - return previousElementSibling != null && rules.test(previousElementSibling, base); - }); - } - - private BiPredicate, SelectableNode> collectMatchers() { - var matcherToHandle = matchers; - matchers = new ArrayList<>(); - return andMatchers(matcherToHandle); - } - - BiPredicate, SelectableNode> parseSelectorInstance(String selector) { - var res = CSS.parseSelector(selector).stream().map(l -> toBaseNodeMatcher(l, this::newInst)).toList(); - return andMatchers(List.of(BaseSelector::isElement, res.size() == 1 ? res.get(0) : orMatchers(res))); - } - - private BiPredicate, SelectableNode> internalToMatcher() { - return matchers.size() == 1 ? matchers.get(0) : andMatchers(matchers); - } - - public NodeMatcher toMatcher() { - return new NodeMatcher<>(internalToMatcher()); - } - - abstract R inst(); - - abstract R newInst(); - - public R universal() { - matchers.add(BaseSelector::isElement); - return inst(); - } - - public R isFirstChild() { - matchers.add(this::isFirstChild); - return inst(); - } - - public R isFirstElementChild() { - matchers.add(this::isFirstElementChild); - return inst(); - } - - public R isLastElementChild() { - matchers.add(this::isLastElementChild); - return inst(); - } - - public R isLastChild() { - matchers.add(this::isLastChild); - return inst(); - } - - private void subsequentSibling() { - var rules = collectMatchers(); - matchers.add((node, base) -> { - var previousElementSibling = wrapper.apply(node.getPreviousElementSibling()); - while(previousElementSibling != null) { - if (rules.test(previousElementSibling, base)) { - return true; - } - previousElementSibling = wrapper.apply(previousElementSibling.getPreviousElementSibling()); - } - return false; - }); - } - - BiPredicate, SelectableNode> toBaseNodeMatcher(List selector, Supplier> stateSupplier) { - var res = stateSupplier.get(); - for (var part : selector) { - if (part instanceof CSS.Combinator c) { - switch (c.type()) { - case CSS.CT_CHILD -> res.withChild(); - case CSS.CT_SIBLING -> res.subsequentSibling(); - case CSS.CT_ADJACENT -> res.nextSibling(); - case CSS.CT_DESCENDANT -> res.withDescendant(); - default -> throw new ParserException("Combinator " + c + " is not supported"); - } - } else if (part instanceof CSS.TagSelector t) { - if (t.namespace() == null || "*".equals(t.namespace())) { - res.element(t.name()); - } else { - res.element(t.name(), t.namespace()); - } - } else if (part instanceof CSS.AttributeSelector a) { - var action = a.action(); - var name = a.name(); - var value = a.value(); - if (action == CSS.ATTR_ACTION_EQUALS) { - res.attrValEq(name, value); - } else if (action == CSS.ATTR_ACTION_ELEMENT && "class".equals(name)) { - res.hasClass(value); - } else if (action == CSS.ATTR_ACTION_EXISTS) { - res.attr(name); - } else if (action == CSS.ATTR_ACTION_START) { - res.attrValStartWith(name, value); - } else if (action == CSS.ATTR_ACTION_ANY) { - res.attrValContains(name, value); - } else if (action == CSS.ATTR_ACTION_END) { - res.attrValEndWith(name, value); - } else { - throw new ParserException("AttributeSelector " + a + " is not supported"); - } - } else if (part instanceof CSS.InternalSelector is && "base".equals(is.name())) { - res.matchers.add((node, base) -> base.isSameNode(unwrapper.apply(node))); - } else if (part instanceof CSS.PseudoSelector ps) { - String name = ps.name(); - if ("contains".equals(name) && ps.data() instanceof CSS.DataString ds) { - res.contains(ds.value()); - } else if ("first-child".equals(name)) { - res.matchers.add(this::isFirstElementChild); - } else if ("last-child".equals(name)) { - res.matchers.add(this::isLastElementChild); - } else if ("empty".equals(name)) { - res.matchers.add(this::isEmpty); - } else if ("only-child".equals(name)) { - res.matchers.add(andMatchers(List.of(this::isFirstElementChild, this::isLastElementChild))); - } else if ("first-of-type".equals(name)) { - res.matchers.add(this::isFirstOfType); - } else if ("last-of-type".equals(name)) { - res.matchers.add(this::isLastOfType); - } else if ("only-of-type".equals(name)) { - res.matchers.add(andMatchers(List.of(this::isFirstOfType, this::isLastOfType))); - } else if ("root".equals(name)) { - res.matchers.add(this::isRoot); - } else if (("is".equals(name) || "where".equals(name) || "not".equals(name)) && ps.data() instanceof CSS.DataSelectors ds) { - var isMatchers = orMatchers(ds.value().stream().map((l) -> toBaseNodeMatcher(l, stateSupplier)).toList()); - var baseRule = res.collectMatchers(); - var mustMatch = !"not".equals(name); - res.matchers.add((node, base) -> baseRule.test(node, base) && isMatchers.test(node, base) == mustMatch); - } else if ("has".equals(name) && ps.data() instanceof CSS.DataSelectors ds) { - // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors/Selector_structure#relative_selector - var hasMatchers = orMatchers(ds.value().stream().map(s -> { - var r = new ArrayList(s.size() + 2); - r.add(new CSS.InternalSelector("base")); - int comb = !s.isEmpty() && s.get(0) instanceof CSS.Combinator combinator ? combinator.type() : -1; - if (comb == -1) { - comb = CSS.CT_DESCENDANT; - r.add(new CSS.Combinator(comb)); - } - r.addAll(s); - var nm = toBaseNodeMatcher(r, stateSupplier); - return switch (comb) { - case CSS.CT_CHILD, CSS.CT_DESCENDANT -> (BiPredicate, SelectableNode>) (node, base) -> node.getAllNodesMatchingAsStream(new NodeMatcher<>(nm), true, unwrapper.apply(base)).count() == 1; - case CSS.CT_SIBLING, CSS.CT_ADJACENT -> (BiPredicate, SelectableNode>) (node, base) -> wrapper.apply(node.getParentNode()).getAllNodesMatchingAsStream(new NodeMatcher<>(nm), true, unwrapper.apply(base)).count() == 1; - default -> throw new ParserException("Combinator " + comb + " is not supported in :has"); - }; - }).toList()); - var baseRule = res.collectMatchers(); - res.matchers.add((node, base) -> baseRule.test(node, base) && hasMatchers.test(node, node)); - } else if (("nth-child".equals(name) || "nth-last-child".equals(name)) && ps.data() instanceof CSS.DataString s) { - var idxPredicate = CSS.parseNth(s.value()); - var reversedOrder = "nth-last-child".equals(name); - res.matchers.add((node, base) -> isAt(node, idxPredicate, reversedOrder)); - } else if (("nth-of-type".equals(name) || "nth-last-of-type".equals(name)) && ps.data() instanceof CSS.DataString s) { - var idxPredicate = CSS.parseNth(s.value()); - var reversedOrder = "nth-last-of-type".equals(name); - res.matchers.add((node, base) -> isAtNthOfType(node, idxPredicate, reversedOrder)); - } else { - throw new ParserException("PseudoSelector '" + name + "' is not supported"); - } - } else if (part instanceof CSS.UniversalSelector) { - res.matchers.add(BaseSelector::isElement); - } else { - throw new ParserException(part + " is not supported"); - } - } - return andMatchers(List.of(BaseSelector::isElement, res.internalToMatcher())); - } - - // - private boolean isFirstChild(SelectableNode node, SelectableNode base) { - return node.getParentNode() != null && node.isSameNode(wrapper.apply(node.getParentNode()).getFirstChild()); - } - - private boolean isFirstElementChild(SelectableNode node, SelectableNode base) { - return node.getParentNode() != null && node.isSameNode(wrapper.apply(node.getParentNode()).getFirstElementChild()); - } - - private boolean isLastChild(SelectableNode node, SelectableNode base) { - return node.getParentNode() != null && node.isSameNode(wrapper.apply(node.getParentNode()).getLastChild()); - } - - private boolean isLastElementChild(SelectableNode node, SelectableNode base) { - return node.getParentNode() != null && node.isSameNode(wrapper.apply(node.getParentNode()).getLastElementChild()); - } - - private static boolean isElement(SelectableNode node) { - return node.getNodeType() == Node.ELEMENT_NODE; - } - - private static boolean isElement(SelectableNode node, SelectableNode base) { - return isElement(node); - } - - private boolean isRoot(SelectableNode n, SelectableNode base) { - return (n.getParentNode() == null || wrapper.apply(n.getParentNode()).getNodeType() == Node.DOCUMENT_NODE) && isElement(n); - } - - private boolean isAt(SelectableNode n, IntPredicate idxPredicate, boolean reversedOrder) { - if (n.getParentNode() != null) { - List c = wrapper.apply(n.getParentNode()).getChildNodes(); - var childNodes = c; - if (reversedOrder) { - int size = c.size(); - childNodes = new Common.NodeList<>(size, (idx) -> c.get(size - idx -1)); - } - var elemIdx = 0; - for (T child : childNodes) { - if (isElement(wrapper.apply(child))) { - elemIdx++; - if (n.isSameNode(child) && idxPredicate.test(elemIdx)) { - return true; - } - } - } - } - return false; - } - - private boolean isAtNthOfType(SelectableNode node, IntPredicate idxPredicate, boolean reversedOrder) { - if (node.getParentNode() != null) { - var nodeName = node.getNodeName(); - var c = wrapper.apply(node.getParentNode()).getChildNodes(); - var childNodes = c; - if (reversedOrder) { - int size = c.size(); - childNodes = new Common.NodeList<>(size, (idx) -> c.get(size - idx -1)); - } - int elemIdx = 0; - for (T child : childNodes) { - if (node.isSameNode(child)) { - elemIdx++; - break; - } - var wrappedChild = wrapper.apply(child); - if (isElement(wrappedChild) && nodeName.equals(wrappedChild.getNodeName())) { - elemIdx++; - } - } - return idxPredicate.test(elemIdx); - } - return false; - } - - private boolean isLastOfType(SelectableNode node, SelectableNode base) { - if (node.getParentNode() != null) { - var nodeName = node.getNodeName(); - var childNodes = wrapper.apply(node.getParentNode()).getChildNodes(); - for (int i = childNodes.size() - 1; i >= 0; i--) { - var e = childNodes.get(i); - var we = wrapper.apply(e); - if (isElement(we) && we.getNodeName().equals(nodeName)) { - return node.isSameNode(e); - } - } - } - return false; - } - - private boolean isFirstOfType(SelectableNode node, SelectableNode base) { - if (node.getParentNode() != null) { - var nodeName = node.getNodeName(); - var childNodes = wrapper.apply(node.getParentNode()).getChildNodes(); - for (var e : childNodes) { - var we = wrapper.apply(e); - if (isElement(we) && we.getNodeName().equals(nodeName)) { - return node.isSameNode(e); - } - } - } - return false; - } - - private boolean isEmpty(SelectableNode node, SelectableNode base) { - return node.getChildNodes().stream().noneMatch(s -> { - var ws = wrapper.apply(s); - return isElement(ws) || ws.getNodeType() == Node.TEXT_NODE; - }); - } - - private BiPredicate, SelectableNode> withDescendant(BiPredicate, SelectableNode> ancestorMatcher) { - return (node, base) -> { - while (node.getParentNode() != null) { - node = wrapper.apply(node.getParentNode()); - if (ancestorMatcher.test(node, base)) { - return true; - } - } - return false; - }; - } - - private static BiPredicate, SelectableNode> orMatchers(List, SelectableNode>> BaseNodeMatchers) { - if (BaseNodeMatchers.size() == 1) { - return BaseNodeMatchers.get(0); - } - if (BaseNodeMatchers.size() == 2) { - var v1 = BaseNodeMatchers.get(0); - var v2 = BaseNodeMatchers.get(1); - return (node, base) -> v1.test(node, base) || v2.test(node, base); - } - - return (node, base) -> { - for (var m : BaseNodeMatchers) { - if (m.test(node, base)) { - return true; - } - } - return false; - }; - } - - private static BiPredicate, SelectableNode> andMatchers(List, SelectableNode>> BaseNodeMatchers) { - if (BaseNodeMatchers.size() == 1) { - return BaseNodeMatchers.get(0); - } - if (BaseNodeMatchers.size() == 2) { - var v1 = BaseNodeMatchers.get(0); - var v2 = BaseNodeMatchers.get(1); - return (node, base) -> v1.test(node, base) && v2.test(node, base); - } - - return (node, base) -> { - for (var m : BaseNodeMatchers) { - if (!m.test(node, base)) { - return false; - } - } - return true; - }; - } - - private static BiPredicate, SelectableNode> matchAttr(String name, BiPredicate> attributeValueMatcher) { - return (node, base) -> { - if (node instanceof SelectableNode.SelectableElement elem) { - var isHtml = Node.NAMESPACE_HTML.equals(elem.getNamespaceURI()); - var toCompareAttr = isHtml ? Common.convertToAsciiLowerCase(name) : name; - return elem.containsAttribute(toCompareAttr) && attributeValueMatcher.test(elem.getAttributeValue(toCompareAttr), elem); - } - return false; - }; - } - -} diff --git a/src/main/java/ch/digitalfondue/jfiveparse/Selector.java b/src/main/java/ch/digitalfondue/jfiveparse/Selector.java index 6534139..571f4bf 100644 --- a/src/main/java/ch/digitalfondue/jfiveparse/Selector.java +++ b/src/main/java/ch/digitalfondue/jfiveparse/Selector.java @@ -15,7 +15,14 @@ */ package ch.digitalfondue.jfiveparse; +import java.util.ArrayList; +import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.function.IntPredicate; +import java.util.function.Supplier; /** * Selector is a type safe builder of node/element selectors. The API is similar @@ -137,37 +144,460 @@ * * */ -public final class Selector extends BaseSelector { +public final class Selector { - private Selector(Map namespaceAlias) { - super(node-> node, node -> (Node) node, namespaceAlias); + private List, SelectableNode>> matchers = new ArrayList<>(); + private final Function> wrapper; + private final Function, T> unwrapper; + private final Map nameSpaceAlias; + + Selector(Function> wrapper, Function, T> unwrapper, Map nameSpaceAlias) { + this.wrapper = wrapper; + this.unwrapper = unwrapper; + this.nameSpaceAlias = nameSpaceAlias; } - public static Selector select() { - return new Selector(Map.of()); + public static Selector select() { + return new Selector(node -> node, node -> (Node) node, Map.of()); } - /** - * Parse a CSS selector string. - * - * @param selector - * @return - */ public static NodeMatcher parseSelector(String selector) { return parseSelector(selector, Map.of()); } public static NodeMatcher parseSelector(String selector, Map namespaceAlias) { - return new NodeMatcher<>(new Selector(namespaceAlias).parseSelectorInstance(selector)); + return new NodeMatcher<>(new Selector(node -> node, node -> (Node) node, namespaceAlias).parseSelectorInstance(selector)); + } + + private Selector newInst() { + return new Selector<>(wrapper, unwrapper, nameSpaceAlias); + } + + private void contains(String value) { + matchers.add((node, base) -> { + if (node instanceof SelectableNode.SelectableElement e) { + var textContent = e.getTextContent(); + return textContent != null && textContent.contains(value); + } + return false; + }); + } + + public Selector element(String name) { + matchers.add((node, base) -> isElement(node) && name.equals(node.getNodeName())); + return this; + } + + public Selector element(String name, String namespace) { + String namespaceToMatch = nameSpaceAlias.getOrDefault(namespace, namespace); + matchers.add((node, base) -> node instanceof SelectableNode.SelectableElement ce && name.equals(ce.getNodeName()) + && Objects.equals(namespaceToMatch, ce.getNamespaceURI())); + return this; + } + + public Selector hasClass(String value) { + attrValInList("class", value); + return this; + } + + public Selector hasClass(String value, String... others) { + hasClass(value); + if (others != null) { + for (String n : others) { + hasClass(n); + } + } + return this; + } + + public Selector id(String value) { + attrValEq("id", value); + return this; + } + + public Selector attr(String name) { + matchers.add(matchAttr(name, (attrValue, elem) -> true)); + return this; + } + + public Selector attrValEq(String name, String value) { + matchers.add(matchAttr(name, (attrValue, elem) -> Objects.equals(attrValue, value))); + return this; + } + + public Selector attrValInList(String name, String value) { + matchers.add(matchAttr(name, (attrValue, elem) -> DOMTokenList.extractValues(elem, name).contains(value))); + return this; + } + + public Selector attrValStartWith(String name, String value) { + matchers.add(matchAttr(name, (attrValue, elem) -> attrValue != null && value != null && attrValue.startsWith(value))); + return this; + } + + public Selector attrValEndWith(String name, String value) { + matchers.add(matchAttr(name, (attrValue, elem) -> attrValue != null && value != null && attrValue.endsWith(value))); + return this; + } + + public Selector attrValContains(String name, String value) { + matchers.add(matchAttr(name, (attrValue, elem) -> attrValue != null && value != null && attrValue.contains(value))); + return this; + } + + public Selector withDescendant() { + var ancestorMatcher = collectMatchers(); + matchers.add(withDescendant(ancestorMatcher)); + return this; + } + + public Selector withChild() { + var rules = collectMatchers(); + matchers.add((node, base) -> node.getParentNode() != null && rules.test(wrapper.apply(node.getParentNode()), base)); + return this; + } + + public Selector nextSibling() { + var rules = collectMatchers(); + matchers.add((node, base) -> { + var previousElementSibling = wrapper.apply(node.getPreviousElementSibling()); + return previousElementSibling != null && rules.test(previousElementSibling, base); + }); + return this; + } + + private BiPredicate, SelectableNode> collectMatchers() { + var matcherToHandle = matchers; + matchers = new ArrayList<>(); + return andMatchers(matcherToHandle); + } + + BiPredicate, SelectableNode> parseSelectorInstance(String selector) { + var res = CSS.parseSelector(selector).stream().map(l -> toBaseNodeMatcher(l, this::newInst)).toList(); + return andMatchers(List.of(Selector::isElement, res.size() == 1 ? res.get(0) : orMatchers(res))); + } + + private BiPredicate, SelectableNode> internalToMatcher() { + return matchers.size() == 1 ? matchers.get(0) : andMatchers(matchers); + } + + public NodeMatcher toMatcher() { + return new NodeMatcher<>(internalToMatcher()); + } + + public Selector universal() { + matchers.add(Selector::isElement); + return this; + } + + public Selector isFirstChild() { + matchers.add(this::isFirstChild); + return this; + } + + public Selector isFirstElementChild() { + matchers.add(this::isFirstElementChild); + return this; + } + + public Selector isLastElementChild() { + matchers.add(this::isLastElementChild); + return this; } - @Override - Selector inst() { + public Selector isLastChild() { + matchers.add(this::isLastChild); return this; } - @Override - Selector newInst() { - return select(); + public Selector subsequentSibling() { + var rules = collectMatchers(); + matchers.add((node, base) -> { + var previousElementSibling = wrapper.apply(node.getPreviousElementSibling()); + while (previousElementSibling != null) { + if (rules.test(previousElementSibling, base)) { + return true; + } + previousElementSibling = wrapper.apply(previousElementSibling.getPreviousElementSibling()); + } + return false; + }); + return this; + } + + BiPredicate, SelectableNode> toBaseNodeMatcher(List selector, Supplier> stateSupplier) { + var res = stateSupplier.get(); + for (var part : selector) { + if (part instanceof CSS.Combinator c) { + switch (c.type()) { + case CSS.CT_CHILD -> res.withChild(); + case CSS.CT_SIBLING -> res.subsequentSibling(); + case CSS.CT_ADJACENT -> res.nextSibling(); + case CSS.CT_DESCENDANT -> res.withDescendant(); + default -> throw new ParserException("Combinator " + c + " is not supported"); + } + } else if (part instanceof CSS.TagSelector t) { + if (t.namespace() == null || "*".equals(t.namespace())) { + res.element(t.name()); + } else { + res.element(t.name(), t.namespace()); + } + } else if (part instanceof CSS.AttributeSelector a) { + var action = a.action(); + var name = a.name(); + var value = a.value(); + if (action == CSS.ATTR_ACTION_EQUALS) { + res.attrValEq(name, value); + } else if (action == CSS.ATTR_ACTION_ELEMENT && "class".equals(name)) { + res.hasClass(value); + } else if (action == CSS.ATTR_ACTION_EXISTS) { + res.attr(name); + } else if (action == CSS.ATTR_ACTION_START) { + res.attrValStartWith(name, value); + } else if (action == CSS.ATTR_ACTION_ANY) { + res.attrValContains(name, value); + } else if (action == CSS.ATTR_ACTION_END) { + res.attrValEndWith(name, value); + } else { + throw new ParserException("AttributeSelector " + a + " is not supported"); + } + } else if (part instanceof CSS.InternalSelector is && "base".equals(is.name())) { + res.matchers.add((node, base) -> base.isSameNode(unwrapper.apply(node))); + } else if (part instanceof CSS.PseudoSelector ps) { + String name = ps.name(); + if ("contains".equals(name) && ps.data() instanceof CSS.DataString ds) { + res.contains(ds.value()); + } else if ("first-child".equals(name)) { + res.matchers.add(this::isFirstElementChild); + } else if ("last-child".equals(name)) { + res.matchers.add(this::isLastElementChild); + } else if ("empty".equals(name)) { + res.matchers.add(this::isEmpty); + } else if ("only-child".equals(name)) { + res.matchers.add(andMatchers(List.of(this::isFirstElementChild, this::isLastElementChild))); + } else if ("first-of-type".equals(name)) { + res.matchers.add(this::isFirstOfType); + } else if ("last-of-type".equals(name)) { + res.matchers.add(this::isLastOfType); + } else if ("only-of-type".equals(name)) { + res.matchers.add(andMatchers(List.of(this::isFirstOfType, this::isLastOfType))); + } else if ("root".equals(name)) { + res.matchers.add(this::isRoot); + } else if (("is".equals(name) || "where".equals(name) || "not".equals(name)) && ps.data() instanceof CSS.DataSelectors ds) { + var isMatchers = orMatchers(ds.value().stream().map((l) -> toBaseNodeMatcher(l, stateSupplier)).toList()); + var baseRule = res.collectMatchers(); + var mustMatch = !"not".equals(name); + res.matchers.add((node, base) -> baseRule.test(node, base) && isMatchers.test(node, base) == mustMatch); + } else if ("has".equals(name) && ps.data() instanceof CSS.DataSelectors ds) { + // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors/Selector_structure#relative_selector + var hasMatchers = orMatchers(ds.value().stream().map(s -> { + var r = new ArrayList(s.size() + 2); + r.add(new CSS.InternalSelector("base")); + int comb = !s.isEmpty() && s.get(0) instanceof CSS.Combinator combinator ? combinator.type() : -1; + if (comb == -1) { + comb = CSS.CT_DESCENDANT; + r.add(new CSS.Combinator(comb)); + } + r.addAll(s); + var nm = toBaseNodeMatcher(r, stateSupplier); + return switch (comb) { + case CSS.CT_CHILD, CSS.CT_DESCENDANT -> (BiPredicate, SelectableNode>) (node, base) -> node.getAllNodesMatchingAsStream(new NodeMatcher<>(nm), true, unwrapper.apply(base)).count() == 1; + case CSS.CT_SIBLING, CSS.CT_ADJACENT -> (BiPredicate, SelectableNode>) (node, base) -> wrapper.apply(node.getParentNode()).getAllNodesMatchingAsStream(new NodeMatcher<>(nm), true, unwrapper.apply(base)).count() == 1; + default -> throw new ParserException("Combinator " + comb + " is not supported in :has"); + }; + }).toList()); + var baseRule = res.collectMatchers(); + res.matchers.add((node, base) -> baseRule.test(node, base) && hasMatchers.test(node, node)); + } else if (("nth-child".equals(name) || "nth-last-child".equals(name)) && ps.data() instanceof CSS.DataString s) { + var idxPredicate = CSS.parseNth(s.value()); + var reversedOrder = "nth-last-child".equals(name); + res.matchers.add((node, base) -> isAt(node, idxPredicate, reversedOrder)); + } else if (("nth-of-type".equals(name) || "nth-last-of-type".equals(name)) && ps.data() instanceof CSS.DataString s) { + var idxPredicate = CSS.parseNth(s.value()); + var reversedOrder = "nth-last-of-type".equals(name); + res.matchers.add((node, base) -> isAtNthOfType(node, idxPredicate, reversedOrder)); + } else { + throw new ParserException("PseudoSelector '" + name + "' is not supported"); + } + } else if (part instanceof CSS.UniversalSelector) { + res.matchers.add(Selector::isElement); + } else { + throw new ParserException(part + " is not supported"); + } + } + return andMatchers(List.of(Selector::isElement, res.internalToMatcher())); + } + + private boolean isFirstChild(SelectableNode node, SelectableNode base) { + return node.getParentNode() != null && node.isSameNode(wrapper.apply(node.getParentNode()).getFirstChild()); + } + + private boolean isFirstElementChild(SelectableNode node, SelectableNode base) { + return node.getParentNode() != null && node.isSameNode(wrapper.apply(node.getParentNode()).getFirstElementChild()); + } + + private boolean isLastChild(SelectableNode node, SelectableNode base) { + return node.getParentNode() != null && node.isSameNode(wrapper.apply(node.getParentNode()).getLastChild()); + } + + private boolean isLastElementChild(SelectableNode node, SelectableNode base) { + return node.getParentNode() != null && node.isSameNode(wrapper.apply(node.getParentNode()).getLastElementChild()); + } + + private static boolean isElement(SelectableNode node) { + return node.getNodeType() == Node.ELEMENT_NODE; + } + + private static boolean isElement(SelectableNode node, SelectableNode base) { + return isElement(node); + } + + private boolean isRoot(SelectableNode n, SelectableNode base) { + return (n.getParentNode() == null || wrapper.apply(n.getParentNode()).getNodeType() == Node.DOCUMENT_NODE) && isElement(n); + } + + private boolean isAt(SelectableNode n, IntPredicate idxPredicate, boolean reversedOrder) { + if (n.getParentNode() != null) { + List c = wrapper.apply(n.getParentNode()).getChildNodes(); + var childNodes = c; + if (reversedOrder) { + int size = c.size(); + childNodes = new Common.NodeList<>(size, (idx) -> c.get(size - idx - 1)); + } + var elemIdx = 0; + for (T child : childNodes) { + if (isElement(wrapper.apply(child))) { + elemIdx++; + if (n.isSameNode(child) && idxPredicate.test(elemIdx)) { + return true; + } + } + } + } + return false; + } + + private boolean isAtNthOfType(SelectableNode node, IntPredicate idxPredicate, boolean reversedOrder) { + if (node.getParentNode() != null) { + var nodeName = node.getNodeName(); + var c = wrapper.apply(node.getParentNode()).getChildNodes(); + var childNodes = c; + if (reversedOrder) { + int size = c.size(); + childNodes = new Common.NodeList<>(size, (idx) -> c.get(size - idx - 1)); + } + int elemIdx = 0; + for (T child : childNodes) { + if (node.isSameNode(child)) { + elemIdx++; + break; + } + var wrappedChild = wrapper.apply(child); + if (isElement(wrappedChild) && nodeName.equals(wrappedChild.getNodeName())) { + elemIdx++; + } + } + return idxPredicate.test(elemIdx); + } + return false; + } + + private boolean isLastOfType(SelectableNode node, SelectableNode base) { + if (node.getParentNode() != null) { + var nodeName = node.getNodeName(); + var childNodes = wrapper.apply(node.getParentNode()).getChildNodes(); + for (int i = childNodes.size() - 1; i >= 0; i--) { + var e = childNodes.get(i); + var we = wrapper.apply(e); + if (isElement(we) && we.getNodeName().equals(nodeName)) { + return node.isSameNode(e); + } + } + } + return false; + } + + private boolean isFirstOfType(SelectableNode node, SelectableNode base) { + if (node.getParentNode() != null) { + var nodeName = node.getNodeName(); + var childNodes = wrapper.apply(node.getParentNode()).getChildNodes(); + for (var e : childNodes) { + var we = wrapper.apply(e); + if (isElement(we) && we.getNodeName().equals(nodeName)) { + return node.isSameNode(e); + } + } + } + return false; + } + + private boolean isEmpty(SelectableNode node, SelectableNode base) { + return node.getChildNodes().stream().noneMatch(s -> { + var ws = wrapper.apply(s); + return isElement(ws) || ws.getNodeType() == Node.TEXT_NODE; + }); + } + + private BiPredicate, SelectableNode> withDescendant(BiPredicate, SelectableNode> ancestorMatcher) { + return (node, base) -> { + while (node.getParentNode() != null) { + node = wrapper.apply(node.getParentNode()); + if (ancestorMatcher.test(node, base)) { + return true; + } + } + return false; + }; + } + + private static BiPredicate, SelectableNode> orMatchers(List, SelectableNode>> BaseNodeMatchers) { + if (BaseNodeMatchers.size() == 1) { + return BaseNodeMatchers.get(0); + } + if (BaseNodeMatchers.size() == 2) { + var v1 = BaseNodeMatchers.get(0); + var v2 = BaseNodeMatchers.get(1); + return (node, base) -> v1.test(node, base) || v2.test(node, base); + } + + return (node, base) -> { + for (var m : BaseNodeMatchers) { + if (m.test(node, base)) { + return true; + } + } + return false; + }; + } + + private static BiPredicate, SelectableNode> andMatchers(List, SelectableNode>> BaseNodeMatchers) { + if (BaseNodeMatchers.size() == 1) { + return BaseNodeMatchers.get(0); + } + if (BaseNodeMatchers.size() == 2) { + var v1 = BaseNodeMatchers.get(0); + var v2 = BaseNodeMatchers.get(1); + return (node, base) -> v1.test(node, base) && v2.test(node, base); + } + + return (node, base) -> { + for (var m : BaseNodeMatchers) { + if (!m.test(node, base)) { + return false; + } + } + return true; + }; + } + + private static BiPredicate, SelectableNode> matchAttr(String name, BiPredicate> attributeValueMatcher) { + return (node, base) -> { + if (node instanceof SelectableNode.SelectableElement elem) { + var isHtml = Node.NAMESPACE_HTML.equals(elem.getNamespaceURI()); + var toCompareAttr = isHtml ? Common.convertToAsciiLowerCase(name) : name; + return elem.containsAttribute(toCompareAttr) && attributeValueMatcher.test(elem.getAttributeValue(toCompareAttr), elem); + } + return false; + }; } } diff --git a/src/main/java/ch/digitalfondue/jfiveparse/W3CDom.java b/src/main/java/ch/digitalfondue/jfiveparse/W3CDom.java index 9b4cca7..0e325a7 100644 --- a/src/main/java/ch/digitalfondue/jfiveparse/W3CDom.java +++ b/src/main/java/ch/digitalfondue/jfiveparse/W3CDom.java @@ -27,40 +27,20 @@ public final class W3CDom { private W3CDom() { } - public static final class W3CDomSelector extends BaseSelector { - - private W3CDomSelector(Map namespaceAlias) { - super(W3CDom::wrap, (toUnwrap) -> toUnwrap == null ? null : ((SelectableNodeWrapper) toUnwrap).node, namespaceAlias); - } - - - /** - * Parse a CSS selector string. - * - * @param selector - * @return - */ - public static NodeMatcher parseSelector(String selector) { - return parseSelector(selector, Map.of()); - } - - public static NodeMatcher parseSelector(String selector, Map namespaceAlias) { - return new NodeMatcher<>(new W3CDomSelector(namespaceAlias).parseSelectorInstance(selector)); - } + public static Selector select() { + return select(Map.of()); + } - @Override - W3CDomSelector inst() { - return this; - } + public static Selector select(Map namespaceAlias) { + return new Selector<>(W3CDom::wrap, (toUnwrap) -> toUnwrap == null ? null : ((SelectableNodeWrapper) toUnwrap).node, namespaceAlias); + } - @Override - W3CDomSelector newInst() { - return select(); - } + public static NodeMatcher parseSelector(String selector) { + return parseSelector(selector, Map.of()); } - public static W3CDomSelector select() { - return new W3CDomSelector(Map.of()); + public static NodeMatcher parseSelector(String selector, Map namespaceAlias) { + return new NodeMatcher<>(select(namespaceAlias).parseSelectorInstance(selector)); } public static Document toW3CDocument(ch.digitalfondue.jfiveparse.Document doc) {