Class: Dommy::Internal::SelectorIndex
- Inherits:
-
Object
- Object
- Dommy::Internal::SelectorIndex
- Defined in:
- lib/dommy/internal/selector_index.rb
Overview
A by-id / by-class / by-tag index over a document's backend element tree,
in document order. It turns a fast_query pre-filter from an O(tree) walk
("visit every node, test it") into an O(matches) lookup ("here are the nodes
with class X"). Built once and memoized per DOM generation
(Document#style_generation) — a mutation invalidates it, and the NEXT query
rebuilds it, so on a page that fires many queries between mutations (jQuery
$.find storms) it is a big win, while a build costs the same single tree
walk it replaces, so it never loses to the plain walk.
Works for ELEMENT-scoped queries too (jQuery's .find is el.querySelectorAll):
each element gets a pre-order interval [enter, exit] during the build, so a
candidate is inside a scope element's subtree iff its enter falls in the
scope's (enter, exit] — an O(candidates) range test, no per-node ancestor
walk. Candidates are stored as [enter, node] in document order.
Holds backend nodes (never wrapped); the caller wraps only candidates and still runs the authoritative #matches?, so the index need only be a SUPERSET (tag matching is case-insensitive here; exact case / namespace stays #matches?'s job).
Constant Summary collapse
- EMPTY =
[].freeze
Class Method Summary collapse
Instance Method Summary collapse
-
#any_ancestor?(prefilter, enter_l) ⇒ Boolean
Is there an indexed element matching
prefilterwhose subtree contains the element at pre-order positionenter_l— i.e. -
#candidates(prefilter, scope_node = nil) ⇒ Object
Backend nodes for an indexable pre-filter ([:id|:class|:type, value]), optionally restricted to
scope_node's subtree; nil when the index can't serve it (a non-indexable :attr, or a scope it doesn't know) so the caller falls back to the walk. -
#enter_of(node) ⇒ Object
The pre-order
enternumber of a backend node (its position), or nil if the node isn't in this index (detached / unknown). -
#initialize ⇒ SelectorIndex
constructor
A new instance of SelectorIndex.
-
#populate(node) ⇒ Object
Walk the backend element tree in document order, numbering each element and recording it under its id / classes / tag.
Constructor Details
#initialize ⇒ SelectorIndex
Returns a new instance of SelectorIndex.
31 32 33 34 35 36 37 |
# File 'lib/dommy/internal/selector_index.rb', line 31 def initialize @by_id = {} @by_class = {} @by_tag = {} @extent = {} # node pointer_id => [enter, exit] pre-order interval @counter = 0 end |
Class Method Details
.build(backend_doc) ⇒ Object
27 28 29 |
# File 'lib/dommy/internal/selector_index.rb', line 27 def self.build(backend_doc) new.tap { |index| index.populate(backend_doc) } end |
Instance Method Details
#any_ancestor?(prefilter, enter_l) ⇒ Boolean
Is there an indexed element matching prefilter whose subtree contains
the element at pre-order position enter_l — i.e. does that element have
an ANCESTOR matching the prefilter? Answered in O(log) via a prefix-max of
the matching elements' exit over their (sorted) enters: an interval
[enter, exit] contains enter_l iff enter < enter_l <= exit, and by the
pre-order property that element is then an ancestor. Used to settle a
descendant combinator's leftmost compound without walking ancestors.
86 87 88 89 90 91 92 93 94 |
# File 'lib/dommy/internal/selector_index.rb', line 86 def any_ancestor?(prefilter, enter_l) enters, max_exits = prefix_max_exit(prefilter) return false unless enters # last index with enters[i] < enter_l gte = enters.bsearch_index { |enter| enter >= enter_l } || enters.length i = gte - 1 i >= 0 && max_exits[i] >= enter_l end |
#candidates(prefilter, scope_node = nil) ⇒ Object
Backend nodes for an indexable pre-filter ([:id|:class|:type, value]),
optionally restricted to scope_node's subtree; nil when the index can't
serve it (a non-indexable :attr, or a scope it doesn't know) so the caller
falls back to the walk.
57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/dommy/internal/selector_index.rb', line 57 def candidates(prefilter, scope_node = nil) entries = entries_for(prefilter) return nil unless entries if scope_node.nil? entries.map { |_enter, node| node } else ext = @extent[scope_node.pointer_id] return nil unless ext # detached / unknown scope -> let the caller walk lo, hi = ext entries.each_with_object([]) { |(enter, node), out| out << node if enter > lo && enter <= hi } end end |
#enter_of(node) ⇒ Object
The pre-order enter number of a backend node (its position), or nil if
the node isn't in this index (detached / unknown).
74 75 76 77 |
# File 'lib/dommy/internal/selector_index.rb', line 74 def enter_of(node) ext = @extent[node.pointer_id] ext && ext[0] end |
#populate(node) ⇒ Object
Walk the backend element tree in document order, numbering each element and recording it under its id / classes / tag.
41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/dommy/internal/selector_index.rb', line 41 def populate(node) child = node.first_element_child while child enter = (@counter += 1) record(child, enter) populate(child) @extent[child.pointer_id] = [enter, @counter] child = child.next_element end self end |