Module: CSS::Selectors::Matcher

Extended by:
Matcher
Included in:
Matcher
Defined in:
lib/css/selectors/matcher.rb

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
%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

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).

Returns:

  • (Boolean)


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