Why do functional pseudos such as :not() and :has() allow quoted arguments?

This isn’t specific to :not(...) and :has(...) selectors- actually, all pseudos
in Sizzle allow for quoted arguments. The pattern for pseudos’ arguments
is defined as:

pseudos = ":(" + characterEncoding + ")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:" + attributes + ")|[^:]|\\\\.)*|.*))\\)|)"

Which can be found on line 91 of sizzle.js as of 831c9c48...

Let’s add some indentation to that, to make it a bit more readable.
Unfortunately, this is still a regexp, so “a bit more readable” still
leaves a lot to be desired:

pseudos = (
    ":(" + characterEncoding + ")" +
    "(?:" +
    "\\(" + // literal open-paren
        "(?:" +

                "(['\"])" + // literal open-quote
                    "((?:\\\\.|[^\\\\])*?)" + // handle backslash escaping
                "\\2" + // close-quote

            "|" + // - OR -

                "(" +
                    "[^()[\\]]*" +
                    "|" +
                    "(?:" +
                        "(?:" + attributes + ")" +
                        "|" +
                        "[^:]" +
                        "|" +
                        "\\\\." +
                    ")*" +
                    "|" +
                    ".*" +
                ")" +

        ")" +
    "\\)" + // literal close-paren
    "|" + // ie, 'or nothing'
")"
);

The main take-away from this is: either single or double-quotes can be
used around the argument in a pseudo-attribute. Backslash escaping is
properly handled, and so any arbitrary string could be passed in as an
argument. Note that the “string” part winds up in the same match index
as the “selector” part in the above regexp; so, in short, that is why
they are treated equally: because the pseudos pattern does not
distinguish between the two. edit: as of jQuery 1.8.2, arguments
with and without quotes are more-explicitly equivalent. I cannot seem
to find this code in the jQuery git repository [help would be appreciated], but the version of
1.8.2 hosted by google, having the sha1sum of a0f48b6ad5322b35383ffcb6e2fa779b8a5fcffc
, has a
"PSEUDO": function on line 4206, which does explicitly detect a
difference between “quoted” and “unquoted” arguments, and ensures they
both wind up in the same place. This logic does not distinguish
between the type of pseudo (“positional” or not) which the argument is
for.

As Sizzle uses Javascript strings to kick off the selection process,
there is no distinction between “string” and “selector” when arguments
are passed in to functions. Making that kind of distinction would be
possible, but as far as I am aware, what is actually desired is always
easily determined from the most basic of context (ie: what type of
pseudo is being used), so there is no real reason to make the
distinction. (please correct in comments if there are any ambiguous
situations which I am unaware of- I’d like to know!)
.

So then, if the lack of distinction between strings and selectors is a
mere implementation detail, why do pseudos such as :eq(...) explicitly
reject such selections?

The answer is simple: it doesn’t, really. At least, not as of jQuery
1.8.1. [edit: as of jQuery 1.8.2, it doesn’t at all. The arguments of
“positional” pseudos can be quoted just like anything else. The below
notes regarding the implementation details of 1.8.1 are left as a
historical curiosity]

Functions such as :eq(...) are implemented as:

"eq": function( elements, argument, not ) {
    var elem = elements.splice( +argument, 1 );
    return not ? elements : elem;
}

At the time that :eq(...) receives the argument, it is still in the
form of a bare argument (quotes and all). Unlike :not(...), this
argument doesn’t go through a compile(...) phase. The “rejection” of
the invalid argument is actually due to the shortcut-casting via
+argument, which will result in NaN for any quoted string (which in
turn, never matches anything). This is yet another implementation
detail, though in this case a “correctly” behaving one (again, as far
as I am aware. Are there situations where non-numeric arguments to such
functions should in fact match?)

edit: As of jQuery 1.8.2, Things have been refactored somewhat, and
“positional” pseudos no-longer receive the “raw” argument. As a result,
quoted arguments are now accepted in :eq(...) and the like. This change
appears to have been a side-effect of another bugfix, as there is no mention of support for quoted arguments in the changelog for af8206ff.., which was intended to fix an error in handling :first and :last, jQuery bug #12303. This commit was found using git bisect and a relatively simple phantomjs script. It is notable that after the Sizzle rewrite in e89d06c4.., Sizzle would not merely fail silently for selectors such as :eq("3"), it would actually throw an exception. That should be taken as yet more evidence that :eq("3") support is not intended behaviour.

There are indeed rationales regarding custom filters, whose arguments
could in some cases be thought of as strings, and sometimes as
selectors, no matter what they superficially look like, depending on
the method in which they are evaluated… but that much is approaching
the pedantic. It should suffice to say that not having a distinction
at the least makes things simpler when calling functions which, no
matter what they may represent, expect a string representation.

In short, the whole situation can be thought of as an implementation
detail, and is rooted in the fact that selectors are passed around as
strings in the first place (how else would you get them into Sizzle?).

Leave a Comment