Skip to contents

This function aims to create an XPath expression equivalent to what would be matched by the given CSS selector. The reason the translation is required is because the XML and xml2 packages, being a libxml2 wrappers, can only evaluate XPath expressions.

Using this function, it is possible to search an XML tree without the prerequisite of knowing XPath.

Usage

css_to_xpath(selector,
             prefix = "descendant-or-self::",
             translator = "generic")

Arguments

selector

A character vector of CSS selectors.

prefix

The prefixes to apply to the resulting XPath expressions. The default or "" are most commonly used.

translator

The type of translator that will be used. Possible options are generic (the default), or html or xhtml.

Details

Each selector given to this function will be translated to an equivalent XPath expression. The resulting XPath expression can be given a prefix which determines the scope of the expression. The default prefix determines the scope to be the node itself and all descendants of the node. Most commonly the prefix is either the default or "", unless it is known what scope a particular XPath expression should have.

A selector starting with the :scope pseudo-class is anchored at the node the expression is evaluated from: the prefix argument is ignored and the expression begins with the XPath self axis instead. For example, ":scope > a" translates to "self::*/a", matching only the a children of the queried node, and a bare ":scope" translates to "self::*", matching the queried node itself. :scope anywhere else in a selector (after a combinator, or within a functional pseudo-class such as :is() or :has()) cannot be expressed in XPath 1.0 and is an error.

The of-type pseudo-classes (:first-of-type, :last-of-type, :only-of-type, :nth-of-type() and :nth-last-of-type()) are only supported when their compound selector names an element, as in "p:first-of-type". Applied to the universal selector, as in "*:first-of-type", they would have to compare each sibling's name against the matched element's own name, which XPath 1.0 cannot express, so the translation is an error. The Python ‘cssselect’ library, from which selectr is ported, has the same limitation.

The Selectors 4 column combinator ("a || b") and the column pseudo-classes :nth-col() and :nth-last-col() are also not supported: which column a cell belongs to depends on table-layout arithmetic (colspan/rowspan carry-over) that XPath 1.0 cannot express. Both are rejected with an error.

:empty deliberately keeps the Selectors 3 semantics that all current browsers implement: an element containing only white space, such as <p> </p>, does not match. (The Selectors 4 specification loosened :empty to also match white-space-only elements, but no browser has shipped that change.)

:lang() ranges are matched as RFC 4647 language ranges, so a wildcard may appear as a whole range (:lang(*)), as a trailing subtag (:lang(en-*)), or in a non-trailing position (:lang(*-CH), :lang(de-*-DE); quoted or not), the last of these being RFC 4647 extended filtering. The html and xhtml translators approximate a non-trailing wildcard from the nearest lang-attributed ancestor (an element matches :lang(*-CH) when its language tag carries a CH subtag in any position). The generic translator has only XPath's lang() function, which cannot express extended filtering, so there a non-trailing wildcard is rejected with an error rather than silently mis-matching.

:dir() translates to a never-matching expression with every translator, including html: an element's resolved directionality also depends on dir="auto", bdi, and form-control rules that a static document cannot answer, so unlike :lang() it is not approximated from ancestor attributes.

The translator used is usually unnecessary to specify as the default is sufficient for most cases. However, it is of use when creating expressions relating to (X)HTML pseudo elements and languages. In particular it qualifies the following pseudo selectors to apply only to relevant (X)HTML elements: :checked, :disabled, :enabled, :link, :optional and :required. Because type is an HTML enumerated attribute, the form-state pseudo-classes match its keywords ASCII case-insensitively, so <input type="RADIO"> is :checked and <input type="HIDDEN"> is excluded from :disabled just as their lower-case spellings are. An <input> with no type defaults to text and so participates in :enabled/:disabled. A disabled <fieldset> disables its descendant form controls for :disabled and :enabled, except those inside its first <legend> child, which stay :enabled; nested disabled fieldsets are handled correctly.

When the translator is set to html, all elements and attributes will be converted to lower case. This restriction is removed when the translator is xhtml (or the default generic translator).

Value

A character vector of XPath expressions.

References

CSS Selectors Level 4 https://www.w3.org/TR/selectors-4/, XPath https://www.w3.org/TR/xpath/.

Author

Simon Potter

Examples

  css_to_xpath(".testclass")
#> [1] "descendant-or-self::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' testclass ')]"
  css_to_xpath("#testid", prefix = "")
#> [1] "*[@id = 'testid']"
  css_to_xpath("#testid .testclass")
#> [1] "descendant-or-self::*[@id = 'testid']//*[@class and contains(concat(' ', normalize-space(@class), ' '), ' testclass ')]"
  css_to_xpath(":scope > .testclass")
#> [1] "self::*/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' testclass ')]"
  css_to_xpath(":checked", translator = "html")
#> [1] "descendant-or-self::*[(@selected and name(.) = 'option') or (@checked and (name(.) = 'input' or name(.) = 'command')and (translate(@type, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') = 'checkbox' or translate(@type, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') = 'radio'))]"