Learn R Programming

selectr (version 0.7-0)

css_to_xpath: Translate a CSS selector to an equivalent XPath expression.

Description

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")

Value

A character vector of XPath expressions.

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.

Errors

Every error css_to_xpath raises is a classed condition inheriting selectr_error (itself an error), so a caller can catch the whole family with one handler, or narrow to a specific class when it needs to react differently:

selectr_parse_error

selector is malformed CSS, e.g. "div >" with nothing following the combinator. Carries selector (the offending input) and pos, the 1-based character position within selector of the token the message names (NULL if no position applies).

selectr_translation_error

selector is valid CSS but cannot be expressed in XPath 1.0 by the requested translator, e.g. :scope nested inside :is(), or an unknown pseudo-class such as ":frobnicate". Carries selector and feature, a short description of the unsupported construct, and - for a construct that can be located, which is every unknown pseudo-class, misplaced :scope, pseudo-element and :lang() argument - pos and column, the 1-based character position of the construct within selector and within its line. Both are NULL for the two failures no single position describes: a namespace prefix that is not an XPath name, and an of-type pseudo-class on the universal selector. feature is the CSS that spells the construct, except for that namespace-prefix failure, which no CSS spells and which reads as the phrase the message uses, e.g. "a namespace prefix that is not an XPath name (`1ns`)".

selectr_argument_error

An R-level argument is invalid: the wrong type, length, or an NA or unrecognised value where one is not allowed. Carries no extra fields.


  tryCatch(
    css_to_xpath("div >"),
    selectr_parse_error = function(e) {
      cat(conditionMessage(e), "\n")
      cat("Position:", e$pos, "\n")
    }
  )
  

Author

Simon Potter

Details

See selectors for a table of every combinator, attribute operator and pseudo-class this function supports, along with an example translation for each; the rest of this section explains the reasoning behind the more surprising entries in that table.

Each selector given to this function will be translated to an equivalent XPath expression. Each of selector, prefix and translator must either have length 1, in which case its single value is used for every translation, or the common length of the longer arguments. Unlike base R, a length that is merely a multiple (or a fraction) of the common length is not recycled but is an error, so that a mistyped argument length does not quietly produce a plausible-looking result. 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.

A type selector carrying no namespace prefix, such as "p", becomes an XPath name test and so matches elements in no namespace only. That is true wherever the name appears in the selector, so ":is(p)", ":not(p)" and ":has(p)" match exactly the elements "p" itself does. An element that is in a namespace, a default namespace declared with xmlns included, has to be selected through a prefix, as in "d|p": prefixes are resolved through the namespace map supplied when the expression is evaluated (the ns argument of xml_find_all or getNodeSet), not through the prefix spelled in the document. Two forms need no such map: "*|p" matches p in any namespace, and "|p" is the explicit spelling of p in no namespace.

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 html and xhtml translators implement RFC 4647 extended filtering for any range naming more than one subtag, approximated from the nearest language-attributed ancestor: a wildcard, explicit or not, may skip any subtag, so :lang(de-DE) - with no wildcard at all - matches lang="de-Latn-DE". The first subtags of range and tag are always paired, though, so a leading wildcard consumes the tag's primary subtag rather than skipping over it: :lang(*-CH) matches lang="de-CH" and lang="fr-Latn-CH", but neither lang="ch" nor lang="ch-DE". A single-subtag range (:lang(en), :lang(en-*)) is a plain prefix test, as before. The generic translator has only XPath's lang() function, which does Selectors 3 |=-style prefix matching and cannot express extended filtering: a non-trailing wildcard is rejected there with an error rather than silently mis-matching, and a multi-subtag range with no wildcard (e.g. :lang(de-DE)) is matched as a prefix only - it does not skip subtags the way the html/xhtml translators do.

Every range must be an RFC 4647 extended language range: each subtag is either a whole * or one to eight alphanumeric characters (letters only, for the first), so an empty, over-long or non-alphanumeric subtag, or a * glued to a subtag, is rejected with an error naming the range (:lang(en-), :lang(--x), :lang(en*), :lang(de-*--de)). The specification says such a range simply matches nothing while leaving the selector valid; XPath has no never-matching form that survives being combined with the rest of the expression, so the range is refused rather than translated into a well-formed one that would select the wrong elements. An empty item of the comma-separated list (:lang(en, )) is likewise an error.

:lang("") matches an element whose content language is not tagged at all: no lang/xml:lang (as applicable to the translator) anywhere in its ancestor-or-self chain, or only an empty one. This holds for every translator.

Which attribute supplies that language differs by translator: the html translator reads lang, while the xhtml translator reads xml:lang or lang, preferring xml:lang where an element carries both, as the HTML language determination does. The generic translator uses XPath's lang() function, which is defined in terms of xml:lang alone. With every translator the language comes from the nearest ancestor-or-self that declares one, and an empty value there resets the language to unknown.

: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 a number of pseudo-classes - :checked, :default, :disabled, :enabled, :link, :optional, :placeholder-shown, :read-only, :read-write and :required - to apply only to relevant (X)HTML elements, identified by local name regardless of namespace so that, for example, "*|input:disabled" and "d1|input:disabled" apply :disabled exactly as "input:disabled" does on an unnamespaced HTML document. See selectors for exactly which elements and attributes each of these matches.

When the translator is set to html, all element and attribute names will be converted to lower case (A-Z only, as an HTML parser folds them), and the attributes HTML defines as ASCII case-insensitive - type, rel, lang and the rest of the list in selectors - also compare their values without regard to case. Both are removed when the translator is xhtml (or the default generic translator), neither of which serves HTML documents.

References

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

See Also

selectors for the full selector-support reference; querySelectorAll, which propagates the same conditions.

Examples

Run this code
  css_to_xpath(".testclass")
  css_to_xpath("#testid", prefix = "")
  css_to_xpath("#testid .testclass")
  css_to_xpath(":scope > .testclass")
  css_to_xpath(":checked", translator = "html")

  # The selectr_parse_error, selectr_translation_error and
  # selectr_argument_error conditions (see 'Errors' below) all inherit
  # 'selectr_error', so callers can catch the family or a specific class.
  tryCatch(
    css_to_xpath("div >"),
    selectr_parse_error = function(e) {
      cat(conditionMessage(e), "\n")
      cat("Position:", e$pos, "\n")
    }
  )

Run the code above in your browser using DataLab