Module: Dommy::Internal::SelectorParser

Defined in:
lib/dommy/internal/selector_parser.rb

Overview

A CSS Selectors (Level 4) validator: parses a selector string against the grammar and raises on anything syntactically invalid, so querySelector/querySelectorAll/matches/closest throw a SyntaxError for exactly the inputs the spec requires — cases Nokogiri’s CSS parser silently accepts (‘[*=test]`, `div % p`, `..x`) or rejects with the wrong error.

This only VALIDATES; matching is still delegated to the backend. It is a hand-written tokenizer + recursive-descent parser covering the productions the Selectors spec (and the WPT corpus) exercise: selector lists, combinators, type/universal selectors with namespace prefixes, id/class, attribute selectors (with matchers and case flags), and pseudo-classes / pseudo-elements (functional and simple). Because querySelector has no namespace declarations, any named namespace prefix is “undeclared” → a SyntaxError (only ‘*|`, `|`, and the default empty prefix are allowed).

Defined Under Namespace

Classes: InvalidSelector, Parser

Constant Summary collapse

KNOWN_PSEUDO_ELEMENTS =

Pseudo-elements (used with ‘::`, plus the four legacy `:` forms). A `::x` outside this set is an unknown pseudo-element → SyntaxError.

%w[
  before after first-line first-letter selection placeholder marker backdrop
  slotted cue file-selector-button first-letter grammar-error spelling-error
  target-text highlight part view-transition view-transition-group
  view-transition-image-pair view-transition-old view-transition-new
].to_set.freeze
SELECTOR_LIST_FUNCTIONS =

Functional pseudo-classes (followed by ‘(…)`). `slotted`/`cue`/`part` are functional pseudo-elements handled in the `::` path.

%w[not is where has matches].to_set.freeze
NTH_FUNCTIONS =
%w[nth-child nth-last-child nth-of-type nth-last-of-type nth-col nth-last-col].to_set.freeze
IDENT_FUNCTIONS =
%w[lang dir].to_set.freeze
NESTED_SELECTOR_FUNCTIONS =
%w[host host-context current].to_set.freeze

Class Method Summary collapse

Class Method Details

.matchable_selector(selector) ⇒ Object

Return ‘selector` with the comma-clauses whose subject is a pseudo-element removed (`::before`, `:first-line` — they match no element, so dropping them is what querySelector should do; the backend would otherwise error or mis-parse `::`). If EVERY clause is a pseudo-element, returns a selector that matches nothing. Assumes `selector` is already known valid. Plain selectors (no `:`) are returned untouched without re-parsing.



65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/dommy/internal/selector_parser.rb', line 65

def matchable_selector(selector)
  s = selector.to_s
  return s unless s.include?(":")

  parser = Parser.new(s)
  parser.parse_selector_list!
  clauses = parser.clauses
  return s unless clauses.any? { |c| c[:pseudo_subject] }

  kept = clauses.reject { |c| c[:pseudo_subject] }
  kept.empty? ? ":not(*)" : kept.map { |c| c[:text] }.join(", ")
rescue InvalidSelector
  s
end

.new_parser(string) ⇒ Object



80
81
82
# File 'lib/dommy/internal/selector_parser.rb', line 80

def new_parser(string)
  Parser.new(string)
end

.valid?(selector) ⇒ Boolean

True when ‘selector` parses cleanly (no raise).

Returns:

  • (Boolean)


52
53
54
55
56
57
# File 'lib/dommy/internal/selector_parser.rb', line 52

def valid?(selector)
  validate!(selector)
  true
rescue ::Dommy::DOMException::SyntaxError
  false
end

.validate!(selector) ⇒ Object

Validate ‘selector`; raise DOMException::SyntaxError if it is not a valid selector list, else return the original string.



44
45
46
47
48
49
# File 'lib/dommy/internal/selector_parser.rb', line 44

def validate!(selector)
  new_parser(selector.to_s).parse_selector_list!
  selector
rescue InvalidSelector => e
  raise ::Dommy::DOMException::SyntaxError, "'#{selector}' is not a valid selector: #{e.message}"
end