Vlad Shevchenko
Vlad Shevchenko

Reputation: 768

Check if CSS selector is valid

I have a field for users to input a CSS selector and I want to check if it's valid (according to css3 specification). I tried to use expressions from css3 specification as suggested in another stackoverflow topic, but it didn't work - the regexp I built just didn't match valid selectors. What I have for now is simply:

try {
     document.querySelector(selector);
} catch (e) {
     // handle bad input
}

But it doesn't seem like a good solution - querySelector function is designed for getting elements, and checking of selector is just a side effect. Furthermore it doesn't provide any information about what is wrong with the selector.

What I'm looking for is something like document.validateSelector or library for parsing CSS selectors.

Upvotes: 16

Views: 13121

Answers (5)

user1032752
user1032752

Reputation: 871

Not sure it would cover all edge cases but using CSS.escape on an invalid selector returns an empty NodeList instead of throwing a syntax error.

document.querySelectorAll(CSS.escape('p > > > a')) => NodeList []
document.querySelectorAll(CSS.escape('a?+')) => NodeList []

Upvotes: 0

woxxom
woxxom

Reputation: 73526

Since querySelector and CSS.supports('selector('+s+')') accept unclosed input like a[href="foo,
let's use the built-in CSSStyleSheet API to check the selector can be used in a CSS rule:

let isv;
function isSelectorValid(sel) {
  if (!isv) {
    try {
      // Chrome 73 and newer
      isv = new CSSStyleSheet();
    } catch (e) {
      // This will fail on sites with an unusually strict CSP that forbids inline styles,
      // so you'll need to set `nonce` or reuse an existing `link` element.
      isv = document.head.appendChild(document.createElement('style')).sheet;
      isv.disabled = true;
    }
  }
  let res = false;
  try {
    // the leading space skips selector's trailing escape char
    const body = ` { --foo: "${Math.random()}"; }`;
    isv.insertRule(sel + body);
    res = isv.cssRules[0].cssText.endsWith(body);
    isv.deleteRule(0);
  } catch (e) {}
  return res;
}

Upvotes: 1

kornieff
kornieff

Reputation: 2557

The problem with original idea is that it will search the entire document. Slow 🤨 !

However, searching an empty light-weight element that is not even attached to the DOM is fast ✌️!

const queryCheck = (s) => document.createDocumentFragment().querySelector(s)

const isSelectorValid = (selector) => {
  try { queryCheck(selector) } catch { return false }
  return true
}

console.assert(isSelectorValid('p > > > a') === false)
console.assert(isSelectorValid('p > a') === true)
console.log('Test passed')

The following version is a bit more advanced with dummy fragment enclosure:

const isSelectorValid = ((dummyElement) =>
  (selector) => {
    try { dummyElement.querySelector(selector) } catch { return false }
    return true
  })(document.createDocumentFragment())
  
console.assert(isSelectorValid('p > > > a') === false)
console.assert(isSelectorValid('p > a') === true)
console.log('Test passed')

Upvotes: 37

Félix
Félix

Reputation: 154

Thanks to @kornieff hint, I've reached out to an answer for nodejs using jsdom, if it can help anyone :

const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const { document } = (new JSDOM('')).window;

const queryCheck = s => document.createDocumentFragment().querySelector(s)

const isSelectorValid = selector => {
  try { queryCheck(selector) } catch { return false }
  return true
}
console.log(isSelectorValid("a#x#y"), isSelectorValid("a?+"));

Upvotes: 1

Dmitri Pavlutin
Dmitri Pavlutin

Reputation: 19080

You can use a library to verify if the selector is valid, and probably get more details from parsing. Check the css selector parser.

Upvotes: 1

Related Questions