#####################################
Select elements with a CSS selector
#####################################
Match descendants against a CSS selector and test a node you already hold. This covers :meth:`~turbohtml.Node.select`,
:meth:`~turbohtml.Node.select_one`, :meth:`~turbohtml.Node.matches`, and :meth:`~turbohtml.Node.closest`, the selector
grammar they share, and how case-sensitivity follows the document mode.
:meth:`~turbohtml.Node.select` returns every descendant matching a CSS selector in document order;
:meth:`~turbohtml.Node.select_one` returns the first or ``None``. The matcher covers type, ``#id``, ``.class``, and
attribute selectors with the ``=``, ``~=``, ``|=``, ``^=``, ``$=``, ``*=`` operators, the tree-structural pseudo-classes
(``:root``, ``:empty``, ``:first-child``, ``:last-child``, ``:only-child``, their ``-of-type`` variants, and the
``:nth-child()`` family with the ``An+B`` microsyntax, and the Level-4 ``of S`` clause that filters the sibling list by
a selector), joined by the descendant, child (``>``), adjacent (``+``), and general-sibling (``~``) combinators, with
comma groups:
.. testcode::
import turbohtml
doc = turbohtml.parse('
')
print([li.text for li in doc.select("li.on")])
print(doc.select_one('a[href^="/"]').text)
print([li.text for li in doc.select("li:nth-child(odd)")])
.. testoutput::
['a']
b
['a']
``:nth-child(An+B of S)`` counts only the inclusive siblings that match the selector list ``S``, so ``An+B`` indexes
that filtered subset rather than every sibling. Here that picks the second ``.row`` item, skipping the separator between
them:
.. testcode::
table = turbohtml.parse("
a
-
b
c
")
print([li.text for li in table.select("li:nth-child(2 of .row)")])
.. testoutput::
['b']
The Selectors Level 4 functional pseudo-classes are supported too: ``:is()`` and ``:where()`` match an element against a
nested selector list (they differ only in specificity, which a tree matcher ignores), ``:has()`` keeps an element when a
relative selector finds a match anchored at it, and ``:not()`` keeps an element that matches none of its arguments.
``:not()`` takes a full selector list, so it negates compound and complex selectors (not just a single class or type)
and nests with the others (``article:not(:has(img))`` selects the image-less articles):
.. testcode::
page = turbohtml.parse("
Post
Note
")
print([a.select_one("h1").text for a in page.select("article:has(img)")])
print([e.tag for e in page.select(":is(h1, figure)")])
print([a.select_one("h1").text for a in page.select("article:not(:has(img))")])
.. testoutput::
['Post']
['h1', 'figure', 'h1']
['Note']
The form and UI pseudo-classes select controls by the state the markup pins down: ``:checked``, ``:disabled`` /
``:enabled``, ``:required`` / ``:optional``, ``:read-only`` / ``:read-write``, and ``:default``. ``:lang()`` matches the
nearest ``lang`` attribute (with hyphen-prefix ranges, so ``:lang(en)`` also matches ``en-GB``) and ``:dir()`` the
resolved text direction. ``:scope`` is the element the query is rooted at, which anchors a relative selector:
.. testcode::
form = turbohtml.parse(
""
)
print([e.attrs["name"] for e in form.select(":checked")])
print([e.attrs["name"] for e in form.select(":required")])
page = turbohtml.parse("
hi
salut
")
print([p.text for p in page.select(":lang(en)")])
card = turbohtml.parse("
T
body
").select_one("#card")
print([e.tag for e in card.select(":scope > p")])
.. testoutput::
['agree']
['email']
['hi']
['p']
The interaction- and navigation-state pseudo-classes (``:hover``, ``:focus``, ``:focus-within``, ``:focus-visible``,
``:active``, ``:target``, ``:target-within``, ``:visited``, ``:link``, and ``:any-link``) parse as valid selectors but
match nothing, since a parsed tree has no live UA state. They stay usable inside ``:is()`` and ``:not()`` rather than
raising, so ``a:not(:visited)`` keeps every link.
``:is()`` and ``:where()`` take a *forgiving* selector list: an arm that fails to parse is dropped and the rest stay
usable, so one unsupported or malformed arm never invalidates the whole selector (``:not()`` and ``:has()`` take a real
list, where a bad arm is still an error):
.. testcode::
doc = turbohtml.parse("
one
two
")
print([e.tag for e in doc.select(":is(p, :totally-unknown)")])
.. testoutput::
['p']
``#id`` and ``.class`` selectors compare case-sensitively in a standards-mode document and ASCII case-insensitively in a
quirks-mode one (a document with no doctype), matching how a browser resolves them. Add a ```` to keep
the comparison exact:
.. testcode::
markup = '
x
'
print(turbohtml.parse(markup).select_one(".lead").tag) # quirks: folds case
print(turbohtml.parse("" + markup).select_one(".lead")) # standards: exact
.. testoutput::
div
None
``:empty`` follows Selectors Level 4: an element counts as empty when its only children are comments or document white
space, so a blank item matches while one holding a non-breaking space (`` `` is not white space) does not:
.. testcode::
items = turbohtml.parse("
x
")
print([li.text for li in items.select("li:empty")])
.. testoutput::
[' ', '']
To test a node you already hold rather than search beneath it, use :meth:`~turbohtml.Node.matches` (does this node
match) or :meth:`~turbohtml.Node.closest` (the nearest matching self-or-ancestor):
.. testcode::
link = turbohtml.parse('').select_one("a")
print(link.matches("nav a"))
print(link.closest("nav").tag)
.. testoutput::
True
nav