Skip to content

Hex-escaped identifiers silently match nothing when followed by a combinator (the form CSS.escape produces) #163

Description

@pierrebeitz

A selector containing a hex escape with its terminating whitespace works when it is the last compound in the selector, but silently returns no matches (no SyntaxError) as soon as a combinator follows. Browsers match all of these.

This is exactly the form CSS.escape() produces for identifiers starting with a digit (CSS.escape('2o') === '\\32 o'), so any code that composes selectors with CSS.escape and runs them under jsdom gets silently-empty results.

Repro

const { JSDOM } = require('jsdom');
const document = new JSDOM('<div id="2o"><i>a</i></div>').window.document;

document.querySelectorAll('div#\\32 o');     // 1  ✅
document.querySelectorAll('#\\32 o i');      // 0  ❌ Chrome: 1
document.querySelectorAll('div#\\32 o i');   // 0  ❌ Chrome: 1
document.querySelectorAll('div#\\32 o > i'); // 0  ❌ Chrome: 1

Single-character escapes are unaffected — with <div id=":2o"><i>b</i></div>:

document.querySelectorAll('#\\3a 2o');    // 1  ✅
document.querySelectorAll('#\\3a 2o i');  // 0  ❌ Chrome: 1
document.querySelectorAll('#\\:2o i');    // 1  ✅  (char escape + combinator is fine)

So the failure is specific to hex escapes, whose whitespace terminator presumably collides with descendant-combinator whitespace handling somewhere between normalization and the compiled matcher.

Expected

Per CSS Syntax §4.3.7 (consume an escaped code point), the whitespace following a hex escape is consumed as part of the escape, so #\32 o i is id 2o + descendant i. Chrome 148 matches 1 for all selectors above.

Versions

Reproduced on 2.2.16, 2.2.23 (npm latest), and master HEAD (1e457d1), via jsdom 26.1.0's querySelectorAll (which delegates to nwsapi).

Context

Found while debugging the :scope escaping issue (#156, #153) on Gmail's colon-prefixed ids — a CSS.escape-based workaround produced these hex-escape selectors and still returned empty results, which is how we isolated this. Happy to test a fix or provide more cases.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions