diff --git a/cssselect/xpath.py b/cssselect/xpath.py index 8ef5d7c..ab6694d 100644 --- a/cssselect/xpath.py +++ b/cssselect/xpath.py @@ -872,10 +872,10 @@ class HTMLTranslator(GenericTranslator): Has a more useful implementation of some pseudo-classes based on HTML-specific element names and attribute names, as described in - the `HTML5 specification`_. It assumes no-quirks mode. + the `HTML specification`_. It assumes no-quirks mode. The API is the same as :class:`GenericTranslator`. - .. _HTML5 specification: http://www.w3.org/TR/html5/links.html#selectors + .. _HTML specification: https://html.spec.whatwg.org/multipage/semantics-other.html#pseudo-classes :param xhtml: If false (the default), element names and attribute names @@ -925,62 +925,67 @@ def xpath_link_pseudo(self, xpath: XPathExpr) -> XPathExpr: # Links are never visited, the implementation for :visited is the same # as in GenericTranslator + # An element is a descendant of a disabled fieldset ancestor unless it is + # a descendant of that fieldset's own first legend element child. lxml's + # XPath evaluator has no generate-id() or current(), so the exemption is + # tested by checking whether any ancestor legend is also the first legend + # child of a disabled ancestor fieldset; with several nested disabled + # fieldsets this can exempt an element that a fully spec-accurate + # algorithm would still treat as disabled. + _disabled_by_fieldset = ( + "ancestor::fieldset[@disabled] and not(" + "count(ancestor::legend | ancestor::fieldset[@disabled]/legend[1]) < " + "count(ancestor::legend) + count(ancestor::fieldset[@disabled]/legend[1])" + ")" + ) + def xpath_disabled_pseudo(self, xpath: XPathExpr) -> XPathExpr: - # http://www.w3.org/TR/html5/section-index.html#attributes-1 + # https://html.spec.whatwg.org/multipage/semantics-other.html#concept-element-disabled return xpath.add_condition( - """ + f""" ( @disabled and ( - (name(.) = 'input' and @type != 'hidden') or + name(.) = 'input' or name(.) = 'button' or name(.) = 'select' or name(.) = 'textarea' or - name(.) = 'command' or name(.) = 'fieldset' or name(.) = 'optgroup' or name(.) = 'option' ) ) or ( ( - (name(.) = 'input' and @type != 'hidden') or + name(.) = 'input' or name(.) = 'button' or name(.) = 'select' or name(.) = 'textarea' ) - and ancestor::fieldset[@disabled] + and {self._disabled_by_fieldset} + ) or ( + name(.) = 'option' and ancestor::optgroup[@disabled] ) """ ) - # FIXME: in the second half, add "and is not a descendant of that - # fieldset element's first legend element child, if any." def xpath_enabled_pseudo(self, xpath: XPathExpr) -> XPathExpr: - # http://www.w3.org/TR/html5/section-index.html#attributes-1 + # https://html.spec.whatwg.org/multipage/semantics-other.html#concept-element-disabled return xpath.add_condition( - """ + f""" ( - @href and ( - name(.) = 'a' or - name(.) = 'link' or - name(.) = 'area' - ) - ) or ( ( - name(.) = 'command' or name(.) = 'fieldset' or name(.) = 'optgroup' ) and not(@disabled) ) or ( ( - (name(.) = 'input' and @type != 'hidden') or + name(.) = 'input' or name(.) = 'button' or name(.) = 'select' or - name(.) = 'textarea' or - name(.) = 'keygen' + name(.) = 'textarea' ) - and not (@disabled or ancestor::fieldset[@disabled]) + and not(@disabled) and not({self._disabled_by_fieldset}) ) or ( name(.) = 'option' and not( @disabled or ancestor::optgroup[@disabled] @@ -988,9 +993,3 @@ def xpath_enabled_pseudo(self, xpath: XPathExpr) -> XPathExpr: ) """ ) - # FIXME: ... or "li elements that are children of menu elements, - # and that have a child element that defines a command, if the first - # such element's Disabled State facet is false (not disabled)". - # FIXME: after ancestor::fieldset[@disabled], add "and is not a - # descendant of that fieldset element's first legend element child, - # if any." diff --git a/tests/test_cssselect.py b/tests/test_cssselect.py index 8e1b2a7..ad38586 100644 --- a/tests/test_cssselect.py +++ b/tests/test_cssselect.py @@ -1393,7 +1393,11 @@ def pcss(main: str, *selectors: str, **kwargs: bool) -> list[str]: assert pcss("span:only-child") == ["foobar-span"] assert pcss("li div:only-child") == ["li-div"] - assert pcss("div *:only-child") == ["li-div", "foobar-span"] + assert pcss("div *:only-child") == [ + "li-div", + "checkbox-legend-enabled", + "foobar-span", + ] # The root element has no siblings, so it matches :only-child # (just like :first-child and :last-child) assert pcss("html:only-child") == ["html"] @@ -1444,7 +1448,8 @@ def pcss(main: str, *selectors: str, **kwargs: bool) -> list[str]: "first-li", "li-div", "p-b", - "checkbox-fieldset-disabled", + "legend", + "checkbox-legend-enabled", "area-href", ] assert pcss("a[href]") == ["tag-anchor", "nofollow-anchor"] @@ -1536,19 +1541,19 @@ def pcss(main: str, *selectors: str, **kwargs: bool) -> list[str]: ] assert pcss(":visited", html_only=True) == [] assert pcss(":enabled", html_only=True) == [ - "link-href", - "tag-anchor", - "nofollow-anchor", "checkbox-unchecked", "text-checked", + "hidden-unchecked", "checkbox-checked", - "area-href", + "checkbox-legend-enabled", ] assert pcss(":disabled", html_only=True) == [ "checkbox-disabled", + "hidden-disabled", "checkbox-disabled-checked", "fieldset", "checkbox-fieldset-disabled", + "hidden-fieldset-disabled", ] assert pcss(":checked", html_only=True) == [ "checkbox-checked", @@ -1727,14 +1732,15 @@ def count(selector: str) -> int: - - + +