lib/infoboxer/navigation/lookup.rb
# frozen_string_literal: true
require_relative 'selector'
module Infoboxer
module Navigation
# See {Lookup::Node Lookup::Node} for everything!
module Lookup
# `Lookup::Node` module provides methods for navigating through
# page tree in XPath-like manner.
#
# What you need to know about it:
#
# ## Selectors
#
# Each `lookup_*` method (and others similar) receive
# _list of selectors_. Examples of acceptable selectors:
#
# ```ruby
# # 1. Node class:
# document.lookup(Bold) # all Bolds
#
# # 2. Class symbol
# document.lookup(:Bold)
# # same as above, useful if you don't want to include Infoboxer::Tree
# # in all of your code or write things like lookup(Infoboxer::Tree::Bold)
#
# # 3. Getter/pattern:
# document.lookup(text: /something/)
# # finds all nodes where result of getter matches pattern
#
# # Checks against patterns are performed with `===`, so you can
# # use regexps to find by text, or ranges to find by number, like
# document.lookup(:Heading, level: (3..4))
#
# # Nodes where method is not defined are ignored, so you can
# # rewrite above example as just
# document.lookup(level: 3..4)
# # ...and receive meaningful result without any NoMethodError
#
# # 4. Check symbol
# document.lookup(:bold?)
# # finds all nodes for which `:bold?` is defined and returns
# # truthy value;
#
# # 5. Code block
# document.lookup{|node| node.params.has_key?(:class)}
# ```
#
# You also can use any of those method without **any** selector,
# thus receiving ALL parents, ALL children, ALL siblings and so on.
#
# ## Chainable navigation
#
# Each `lookup_*` method returns an instance of {Tree::Nodes} class,
# which behaves like an Array, but also defines similar set of
# `lookup_*` methods, so, you can brainlessly do the things like
#
# ```ruby
# document.
# lookup(:Paragraph){|p| p.text.length > 100}.
# lookup(:Wikilink, text: /^List of/).
# select(&:bold?)
# ```
#
# ## Underscored methods
#
# For all methods of this module you can notice "underscored" version
# (`lookup_children` vs `_lookup_children` and so on). Basically,
# underscored versions accept instance of {Lookup::Selector}, which
# is already preprocessed version of all selectors. It is kinda
# internal thing, though can be useful if you store selectors in
# variables -- it is easier to have and use just one instance of
# Selector, than list of arguments and blocks.
#
module Node
# @!method matches?(*selectors, &block)
# Checks if current node matches selectors.
# @!method lookup(*selectors, &block)
# Selects matching nodes from entire subtree inside current node.
# @!method lookup_children(*selectors, &block)
# Selects nodes only from this node's direct children.
# @!method lookup_parents(*selectors, &block)
# Selects matching nodes of this node's parents chain, up to
# entire {Tree::Document Document}.
# @!method lookup_siblings(*selectors, &block)
# Selects matching nodes from current node's siblings.
# @!method lookup_next_siblings(*selectors, &block)
# Selects matching nodes from current node's siblings, which
# are below current node in parents children list.
# @!method lookup_prev_siblings(*selectors, &block)
# Selects matching nodes from current node's siblings, which
# are above current node in parents children list.
# @!method lookup_prev_sibling(*selectors, &block)
# Selects first matching nodes from current node's siblings, which
# are above current node in parents children list.
# Underscored version of {#matches?}
def _matches?(selector)
selector === self
end
# Underscored version of {#lookup}
def _lookup(selector)
Tree::Nodes[_matches?(selector) ? self : nil, *children._lookup(selector)]
.flatten.compact
end
# Underscored version of {#lookup_children}
def _lookup_children(selector)
@children._find(selector)
end
# Underscored version of {#lookup_parents}
def _lookup_parents(selector)
case
when !parent
Tree::Nodes[]
when parent._matches?(selector)
Tree::Nodes[parent, *parent._lookup_parents(selector)]
else
parent._lookup_parents(selector)
end
end
# Underscored version of {#lookup_siblings}
def _lookup_siblings(selector)
siblings._find(selector)
end
# Underscored version of {#lookup_prev_siblings}
def _lookup_prev_siblings(selector)
prev_siblings._find(selector)
end
# Underscored version of {#lookup_prev_sibling}
def _lookup_prev_sibling(selector)
prev_siblings.reverse.detect { |n| selector === n }
end
# Underscored version of {#lookup_next_siblings}
def _lookup_next_siblings(selector)
next_siblings._find(selector)
end
%i[
matches?
lookup lookup_children lookup_parents
lookup_siblings
lookup_next_siblings lookup_prev_siblings
lookup_prev_sibling
]
.map { |sym| [sym, :"_#{sym}"] }
.each do |sym, underscored|
define_method(sym) do |*args, &block|
send(underscored, Selector.new(*args, &block))
end
end
# Checks if node has any parent matching selectors.
def parent?(*selectors, &block)
!lookup_parents(*selectors, &block).empty?
end
end
# This module provides implementations for all `lookup_*` methods
# of {Lookup::Node} for be used on nodes list. Note, that all
# those methods return _flat_ list of results (so, if you have
# found several nodes, and then look for their siblings, you should
# not expect array of arrays -- just one array of nodes).
#
# See {Lookup::Node} for detailed lookups and selectors explanation.
module Nodes
# @!method lookup(*selectors, &block)
# @!method lookup_children(*selectors, &block)
# @!method lookup_parents(*selectors, &block)
# @!method lookup_siblings(*selectors, &block)
# @!method lookup_next_siblings(*selectors, &block)
# @!method lookup_prev_siblings(*selectors, &block)
# @!method _lookup(selector)
# @!method _lookup_children(selector)
# @!method _lookup_parents(selector)
# @!method _lookup_siblings(selector)
# @!method _lookup_next_siblings(selector)
# @!method _lookup_prev_siblings(selector)
# Underscored version of {#find}.
def _find(selector)
select { |n| n._matches?(selector) }
end
# Selects nodes of current list (and only it, no children checks),
# which are matching selectors.
def find(*selectors, &block)
_find(Selector.new(*selectors, &block))
end
%i[
_lookup _lookup_children _lookup_parents
_lookup_siblings _lookup_prev_siblings _lookup_next_siblings
].each do |sym|
define_method(sym) do |*args|
make_nodes(map { |n| n.send(sym, *args) })
end
end
# not delegate, but redefine: Selector should be constructed only once
%i[
lookup lookup_children lookup_parents
lookup_siblings
lookup_next_siblings lookup_prev_siblings
].map { |sym| [sym, :"_#{sym}"] }.each do |sym, underscored|
define_method(sym) do |*args, &block|
send(underscored, Selector.new(*args, &block))
end
end
end
end
end
end