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.
css_to_xpath(selector,
prefix = "descendant-or-self::",
translator = "generic")A character vector of XPath expressions.
A character vector of CSS selectors.
The prefixes to apply to the resulting XPath expressions. The
default or "" are most commonly used.
The type of translator that will be used. Possible options are
generic (the default), or html or xhtml.
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_errorselector 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_errorselector 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_errorAn 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")
}
)
Simon Potter
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.
CSS Selectors Level 4 https://www.w3.org/TR/selectors-4/, XPath https://www.w3.org/TR/xpath/.
selectors for the full selector-support reference;
querySelectorAll, which propagates the same conditions.
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