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.
parse! returns the AST that SelectorMatcher matches against (it both
validates and is the matcher's front end). 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::xoutside 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/partare 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
- AST_CACHE_CAP =
A parsed AST is a pure function of (selector string, namespaces) and never goes stale as the DOM mutates (unlike the query-result caches, which tag entries with style_generation). So memoize globally:
matches?/closestre-parse on every call, and query* re-parse on every cache miss (i.e. after any mutation), so the same handful of selectors are otherwise re-parsed constantly. Bounded via the same "clear at cap" idiom as the query caches. Lock-free plain Hash:parse!is pure Ruby (never releases the GVL), so a read/write is atomic under the GVL — a benign duplicate parse is the worst a race can cause, matching the existing per-document caches. 2048
Class Method Summary collapse
- .ast_cache_store(cache, key, value) ⇒ Object
-
.matchable_selector(selector) ⇒ Object
Return
selectorwith 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::). - .new_parser(string, namespaces = nil) ⇒ Object
-
.parse!(selector, namespaces: nil) ⇒ Object
namespacesmaps a prefix String to its URI (with the symbol key :default for the default namespace), lettingsvg|rectresolve in a stylesheet that declared@namespace. -
.valid?(selector) ⇒ Boolean
True when
selectorparses cleanly (no raise). -
.validate!(selector) ⇒ Object
Validate
selector; raise DOMException::SyntaxError if it is not a valid selector list, else return the original string.
Class Method Details
.ast_cache_store(cache, key, value) ⇒ Object
84 85 86 87 |
# File 'lib/dommy/internal/selector_parser.rb', line 84 def ast_cache_store(cache, key, value) cache.clear if cache.size >= AST_CACHE_CAP cache[key] = value end |
.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.
110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/dommy/internal/selector_parser.rb', line 110 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, namespaces = nil) ⇒ Object
125 126 127 |
# File 'lib/dommy/internal/selector_parser.rb', line 125 def new_parser(string, namespaces = nil) Parser.new(string, namespaces: namespaces) end |
.parse!(selector, namespaces: nil) ⇒ Object
namespaces maps a prefix String to its URI (with the symbol key
:default for the default namespace), letting svg|rect resolve in a
stylesheet that declared @namespace. nil/empty (the DOM querySelector
path) keeps any named prefix undeclared — a SyntaxError, as before.
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/dommy/internal/selector_parser.rb', line 60 def parse!(selector, namespaces: nil) key = [selector.to_s, namespaces] cache = (@ast_cache ||= {}) if cache.key?(key) cached = cache[key] # An invalid selector is memoized as its SyntaxError so a repeat still # throws (the message embeds the selector, so re-raising is exact). raise cached if cached.is_a?(::Dommy::DOMException::SyntaxError) return cached end ast = begin new_parser(selector.to_s, namespaces).parse_selector_list! rescue InvalidSelector => e error = ::Dommy::DOMException::SyntaxError.new("'#{selector}' is not a valid selector: #{e.}") ast_cache_store(cache, key, error) raise error end ast_cache_store(cache, key, ast) ast end |
.valid?(selector) ⇒ Boolean
True when selector parses cleanly (no raise).
97 98 99 100 101 102 |
# File 'lib/dommy/internal/selector_parser.rb', line 97 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.
91 92 93 94 |
# File 'lib/dommy/internal/selector_parser.rb', line 91 def validate!(selector) parse!(selector) selector end |