Module: CSS::Selectors::Matcher
Overview
Matches a Selector AST against any duck-typed element. Required methods on the element:
- `name` (or `tag_name`) — tag name
- `[](attr)` — attribute value or nil
- `parent` — parent element or non-element
- `previous_element` (or `previous_element_sibling`) — preceding
element sibling
- `next_element` (or `next_element_sibling`) — following
element sibling
- `children` (and optionally `element_children`) — child nodes
‘Nokogiri::XML::Element` and `Nokogiri::HTML::Element` satisfy this protocol out of the box.
Pseudo-classes that depend on user-agent state (‘:hover`, `:focus`, `:visited`, etc.) return false by default; pass an explicit `state:` mapping to opt into stateful matching. Constraint-validation states (`:valid`, `:invalid`, `:user-valid`, `:user-invalid`, `:indeterminate`) can’t be derived from the DOM, so they are also ‘state:` opt-ins.
Defined Under Namespace
Classes: Context
Constant Summary collapse
- DISABLEABLE_TAGS =
%w[button input select textarea optgroup option fieldset].freeze
- INPUT_TAGS =
%w[input textarea select].freeze
- LINK_TAGS =
%w[a area link].freeze
- RO_INPUT_TYPES =
%w[hidden range color checkbox radio file submit image reset button].freeze
- STATEFUL_PSEUDOS =
User-agent state pseudos. The matcher returns ‘false` for these unless the caller passes a `state:` Hash describing which elements (or “all”) should match.
%w[ hover focus focus-within focus-visible active visited target valid invalid user-valid user-invalid indeterminate ].to_set.freeze
- PROPAGATING_STATEFUL_PSEUDOS =
Per spec these states propagate up the ancestor chain — if a descendant is hovered/active/contains-focus, the ancestors share the state for selector-matching purposes.
%w[hover active focus-within].to_set.freeze
- EMPTY_CLASSES =
[].freeze
- SCOPE_KEY =
Private key under which the ‘:scope` set is carried on the `state` object, so it threads through the deep match recursion without changing every method signature. A user `state` Hash is keyed by pseudo names, so this unique object never collides.
Object.new.freeze
- EMPTY_WS_KEY =
Private key carrying the ‘:empty` whitespace policy on `state`. Only stamped when the caller opts out of the default (whitespace allowed), so the key’s absence means “Selectors-4 default”.
Object.new.freeze
Instance Method Summary collapse
- #classes_of(element, cache = nil) ⇒ Object
-
#closest(element, selector, state: nil, scope: nil, empty_allows_whitespace: true) ⇒ Object
Nearest inclusive-ancestor of ‘element` matching `selector` (Element#closest).
- #id_of(element, cache = nil) ⇒ Object
-
#matches?(element, selector, cache: nil, state: nil, scope: nil, empty_allows_whitespace: true) ⇒ Boolean
‘scope` (an element, Array, or Set) is the set `:scope` matches; with none, `:scope` falls back to `:root`.
-
#select_all(roots, selector, state: nil, scope: nil, empty_allows_whitespace: true) ⇒ Object
querySelectorAll-style: every descendant of ‘roots` (an element or array of elements), in document order, matching `selector`.
- #select_first(roots, selector, state: nil, scope: nil, empty_allows_whitespace: true) ⇒ Object
- #tag_of(element, cache = nil) ⇒ Object
Instance Method Details
#classes_of(element, cache = nil) ⇒ Object
256 257 258 259 |
# File 'lib/css/selectors/matcher.rb', line 256 def classes_of(element, cache = nil) ctx = context_for(element, cache) ctx ? ctx.classes : build_class_set(element) end |
#closest(element, selector, state: nil, scope: nil, empty_allows_whitespace: true) ⇒ Object
Nearest inclusive-ancestor of ‘element` matching `selector` (Element#closest).
114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/css/selectors/matcher.rb', line 114 def closest(element, selector, state: nil, scope: nil, empty_allows_whitespace: true) sel = selector.is_a?(String) ? Parser.parse_selector_list(selector) : selector state = build_state(state, scope, empty_allows_whitespace) cache = {} cur = element while cur return cur if matches?(cur, sel, cache: cache, state: state) cur = parent_element(cur) end nil end |
#id_of(element, cache = nil) ⇒ Object
251 252 253 254 |
# File 'lib/css/selectors/matcher.rb', line 251 def id_of(element, cache = nil) ctx = context_for(element, cache) ctx ? ctx.id : attr(element, 'id') end |
#matches?(element, selector, cache: nil, state: nil, scope: nil, empty_allows_whitespace: true) ⇒ Boolean
‘scope` (an element, Array, or Set) is the set `:scope` matches; with none, `:scope` falls back to `:root`. `empty_allows_whitespace` (default true, Selectors-4) controls `:empty`: when false, any non-empty text — including whitespace — disqualifies (real browsers).
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/css/selectors/matcher.rb', line 67 def matches?(element, selector, cache: nil, state: nil, scope: nil, empty_allows_whitespace: true) sel = selector.is_a?(String) ? Parser.parse_selector_list(selector) : selector state = build_state(state, scope, empty_allows_whitespace) case sel when SelectorList sel.selectors.any? { match_complex(element, _1, cache, state) } when ComplexSelector match_complex(element, sel, cache, state) when CompoundSelector match_compound(element, sel, cache, state) else raise ArgumentError, "expected a selector node or string, got #{sel.class}" end end |
#select_all(roots, selector, state: nil, scope: nil, empty_allows_whitespace: true) ⇒ Object
querySelectorAll-style: every descendant of ‘roots` (an element or array of elements), in document order, matching `selector`. `:scope` honours the `scope:` option (default `:root`).
86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/css/selectors/matcher.rb', line 86 def select_all(roots, selector, state: nil, scope: nil, empty_allows_whitespace: true) sel = selector.is_a?(String) ? Parser.parse_selector_list(selector) : selector state = build_state(state, scope, empty_allows_whitespace) cache = {} list = roots.is_a?(Array) ? roots : [roots] out = [] list.each { collect_matches(_1, sel, cache, state, out) } list.size > 1 ? dedup_nodes(out) : out end |
#select_first(roots, selector, state: nil, scope: nil, empty_allows_whitespace: true) ⇒ Object
98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/css/selectors/matcher.rb', line 98 def select_first(roots, selector, state: nil, scope: nil, empty_allows_whitespace: true) sel = selector.is_a?(String) ? Parser.parse_selector_list(selector) : selector state = build_state(state, scope, empty_allows_whitespace) cache = {} list = roots.is_a?(Array) ? roots : [roots] list.each do |root| hit = first_match(root, sel, cache, state) return hit if hit end nil end |
#tag_of(element, cache = nil) ⇒ Object
246 247 248 249 |
# File 'lib/css/selectors/matcher.rb', line 246 def tag_of(element, cache = nil) ctx = context_for(element, cache) ctx ? ctx.tag : tag(element) end |