Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 28 additions & 29 deletions cssselect/xpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -925,72 +925,71 @@ 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]
)
)
"""
)
# 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."
24 changes: 15 additions & 9 deletions tests/test_cssselect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -1727,14 +1732,15 @@ def count(selector: str) -> int:
<input type="checkbox" id="checkbox-unchecked" />
<input type="checkbox" id="checkbox-disabled" disabled="" />
<input type="text" id="text-checked" checked="checked" />
<input type="hidden" />
<input type="hidden" disabled="disabled" />
<input type="hidden" id="hidden-unchecked" />
<input type="hidden" id="hidden-disabled" disabled="disabled" />
<input type="checkbox" id="checkbox-checked" checked="checked" />
<input type="checkbox" id="checkbox-disabled-checked"
disabled="disabled" checked="checked" />
<fieldset id="fieldset" disabled="disabled">
<legend id="legend"><input type="checkbox" id="checkbox-legend-enabled" /></legend>
<input type="checkbox" id="checkbox-fieldset-disabled" />
<input type="hidden" />
<input type="hidden" id="hidden-fieldset-disabled" />
</fieldset>
</p>
<ol id="second-ol">
Expand Down
Loading