Translate a CSS selector to XPath

Emit an XPath 1.0 expression that selects the same nodes as a CSS selector with turbohtml.convert.css_to_xpath(), for systems that speak only XPath.

turbohtml.convert.css_to_xpath() replaces cssselect.HTMLTranslator().css_to_xpath: it emits an XPath 1.0 expression that selects the same nodes as the CSS selector, for the systems that only speak XPath:

from turbohtml.convert import css_to_xpath

print(css_to_xpath("ul > li.item"))
descendant-or-self::ul/li[@class and contains(concat(' ', normalize-space(@class), ' '), ' item ')]

The default prefix scopes the expression to the context node’s subtree, the cssselect convention. Pass a different prefix to change the anchoring; descendant:: mirrors what select() walks (descendants only), and // produces a document-absolute path:

print(css_to_xpath("li:first-child", prefix="//"))
//li[not(preceding-sibling::*)]

A selector list becomes a union with the prefix on each arm, and a selector that the CSS grammar rejects raises SelectorSyntaxError while a valid one that XPath 1.0 cannot express (:dir(), an of-type pseudo-class without a type) raises ExpressionError:

from turbohtml.convert import ExpressionError

print(css_to_xpath("h1, h2", prefix="//"))
try:
    css_to_xpath("*:first-of-type")
except ExpressionError as error:
    print(error)
//h1 | //h2
the of-type pseudo-classes need a type selector

Weigh a selector’s specificity

turbohtml.convert.css_specificity() returns the (a, b, c) triple CSS Selectors Level 4 §17 defines – a for id selectors, b for class, attribute, and pseudo-class selectors, c for type and pseudo-element selectors – one per comma-separated selector, matching cssselect’s Selector.specificity(). :is(), :not(), and :has() take their most specific argument; :where() contributes zero.

from turbohtml.convert import css_specificity

print(css_specificity("#nav a.link, :where(#x) p"))
[(1, 1, 1), (0, 0, 1)]