class Webgen::NodeFinder
Used for finding nodes that match certain criterias.
About¶ ↑
This extension class is used for finding nodes that match certain criterias (all nodes are used if no filter options are specified) when calling the find method. There are some built-in filters but one can also provide custom filters via add_filter_module.
The found nodes are either returned in a flat list or hierarchical in nested lists (if a node has no child nodes, only the node itself is used; otherwise a two element array containing the node and child nodes is used). Sorting, limiting the number of returned nodes and using an offset are also possible.
Note that results are cached in the volatile cache of the Cache instance!
Finder options¶ ↑
A complete list of the supported finder options can be found in the user documentation! Note that there may also be other 3rd party node filters available if you are using extension bundles!
Implementing a filter module¶ ↑
Implementing a filter module is very easy. Just create a module that contains your filter methods and tell the NodeFinder object about it using the add_filter_module method. A filter method needs to take three arguments: the Result stucture, the reference node and the filter value.
The result.nodes accessor contains the array of nodes that
should be manipulated in-place.
If a filter uses the reference node, it has to tell the node finder about it to allow proper caching:
-
If the node's language property is used,
result.lang_usedhas to be set totrue. -
If the node's hierarchy level is used,
result.level_usedhas to be set totrue. -
If the node's parent is used,
result.parent_node_usedhas to be set totrue. -
In all other cases,
result.ref_node_usedhas to be set totrue
Here is a sample filter module which provides the ability to filter nodes
based on the meta information key category. The
category key contains an array with one or more categories.
The value for this category filter is one or more strings and the filter
returns those nodes that contain at least one specified category.
module CategoryFilter def filter_on_category(result, ref_node, categories) categories = [categories].flatten # needed in case categories is a string result.nodes.select! {|n| categories.any? {|c| n['category'].include?(c)}} end end website.ext.node_finder.add_filter_module(CategoryFilter, category: 'filter_on_category')
Constants
- Result
Result class used when filtering the nodes.
The attribute
ref_node_usedmust not be set tofalseonce it istrue!
Public Class Methods
Create a new NodeFinder object for the given website.
# File lib/webgen/node_finder.rb, line 75 def initialize(website) @website = website @mapping = { :alcn => :filter_alcn, :absolute_levels => :filter_absolute_levels, :lang => :filter_lang, :and => :filter_and, :or => :filter_or, :not => :filter_not, :ancestors => :filter_ancestors, :descendants => :filter_descendants, :siblings => :filter_siblings, :mi => :filter_meta_info } end
Public Instance Methods
Add a module with filter methods.
The parameter mapping needs to be a hash associating unique
names with the methods of the given module that can be used as finder
methods.
Examples:¶ ↑
node_finder.add_filter_module(MyModule, blog: 'filter_on_blog')
# File lib/webgen/node_finder.rb, line 95 def add_filter_module(mod, mapping) public_methods = mod.public_instance_methods.map {|c| c.to_s} mapping.each do |name, method| if !public_methods.include?(method.to_s) raise ArgumentError, "Finder method '#{method}' not found in module #{mod}" end @mapping[name.intern] = method end extend(mod) end
Return all nodes that match certain criterias.
The parameter opts_or_name can either be a hash with finder
options or the name of a finder option set defined using the configuration
option 'node_finder.options_sets'. The node ref_node
is used as reference node.
# File lib/webgen/node_finder.rb, line 111 def find(opts_or_name, ref_node) if result = cached_result(opts_or_name, ref_node) return result.nodes end opts = prepare_options_hash(opts_or_name) limit, offset, flatten, sort, levels, reverse = remove_non_filter_options(opts) flatten = true if limit || offset levels = [levels || [1, 1_000_000]].flatten.map {|i| i.to_i} result = filter_nodes(opts, ref_node) nodes = result.nodes if flatten sort_nodes(nodes, sort, reverse) nodes = nodes[(offset.to_s.to_i)..(limit ? offset.to_s.to_i + limit.to_s.to_i - 1 : -1)] if limit || offset else temp = {} min_level = 1_000_000 nodes.each {|n| min_level = n.level if n.level < min_level} nodes.each do |n| hierarchy_nodes = [] (hierarchy_nodes.unshift(n); n = n.parent) while n.level >= min_level hierarchy_nodes.inject(temp) {|memo, hn| memo[hn] ||= {}} end reducer = lambda do |h, level| if level < levels.first temp = h.map {|k,v| v.empty? ? nil : reducer.call(v, level + 1)}.compact temp.length == 1 && temp.first.kind_of?(Array) ? temp.first : temp elsif level < levels.last h.map {|k,v| v.empty? ? k : [k, reducer.call(v, level + 1)]} else h.map {|k,v| k} end end nodes = reducer.call(temp, 1) sort_nodes(nodes, sort, reverse, false) end result.nodes = nodes cache_result(opts_or_name, ref_node, result) result.nodes end
Private Instance Methods
# File lib/webgen/node_finder.rb, line 170 def cache_key(opts, ref_node, result) [opts, result.ref_node_used && ref_node.alcn, result.lang_used && ref_node.lang, result.level_used && ref_node.level, result.parent_node_used && ref_node.parent.alcn] end
# File lib/webgen/node_finder.rb, line 165 def cache_result(opts, ref_node, result) result_cache[opts] = result result_cache[cache_key(opts, ref_node, result)] = result end
# File lib/webgen/node_finder.rb, line 161 def cached_result(opts, ref_node) (result = result_cache[opts]) && result_cache[cache_key(opts, ref_node, result)] end
# File lib/webgen/node_finder.rb, line 197 def filter_nodes(opts, ref_node) nodes = @website.tree.node_access[:alcn].values nodes.delete(@website.tree.dummy_root) result = Result.new(nodes, false) opts.each do |filter, value| if @mapping.has_key?(filter) send(@mapping[filter], result, ref_node, value) else @website.logger.warn { "Ignoring unknown node finder filter '#{filter}'" } end end result end
# File lib/webgen/node_finder.rb, line 182 def prepare_options_hash(opts_or_name) if Hash === opts_or_name opts_or_name.symbolize_keys elsif @website.config['node_finder.option_sets'].has_key?(opts_or_name) @website.config['node_finder.option_sets'][opts_or_name].symbolize_keys else raise ArgumentError, "Invalid argument supplied, expected Hash or name of search definition, not #{opts_or_name}" end end
# File lib/webgen/node_finder.rb, line 192 def remove_non_filter_options(opts) [opts.delete(:limit), opts.delete(:offset), opts.delete(:flatten), opts.delete(:sort), opts.delete(:levels), opts.delete(:reverse)] end
# File lib/webgen/node_finder.rb, line 178 def result_cache @website.cache.volatile[:node_finder] ||= {} end
# File lib/webgen/node_finder.rb, line 214 def sort_nodes(nodes, sort, reverse, flat_mode = true) return unless sort if sort == true nodes.sort! do |(a,_),(b,_)| a = (a['sort_info'] && a['sort_info'].to_s) || a['title'].to_s || '' b = (b['sort_info'] && b['sort_info'].to_s) || b['title'].to_s || '' (a = a.to_i; b = b.to_i) if a !~ /\D/ && b !~ /\D/ (reverse ? b <=> a : a <=> b) end else nodes.sort! do |(a,_),(b,_)| a, b = a[sort].to_s, b[sort].to_s a, b = a.to_i, b.to_i if a !~ /\D/ && b !~ /\D/ (reverse ? b <=> a : a <=> b) end end nodes.each {|n, children| sort_nodes(children, sort, reverse, flat_mode) if children } unless flat_mode end
Filter methods
↑ topPrivate Instance Methods
# File lib/webgen/node_finder.rb, line 277 def filter_absolute_levels(result, ref_node, range) range = [range].flatten.map do |i| if (i = i.to_i) < 0 result.level_used = true ref_node.level + 1 + i else i end end result.nodes.keep_if {|n| n.level >= range.first && n.level <= range.last} end
# File lib/webgen/node_finder.rb, line 269 def filter_alcn(result, ref_node, alcn) alcn = [alcn].flatten.map do |a| result.ref_node_used = true unless a.to_s.start_with?('/') Webgen::Path.append(ref_node.alcn, a.to_s) end result.nodes.keep_if {|n| alcn.any? {|a| n =~ a}} end
# File lib/webgen/node_finder.rb, line 306 def filter_ancestors(result, ref_node, enabled) return unless enabled result.ref_node_used = true nodes = [] node = ref_node until node == node.tree.dummy_root nodes.unshift(node) node = node.parent end result.nodes = nodes & result.nodes end
# File lib/webgen/node_finder.rb, line 235 def filter_and(result, ref_node, opts) [opts].flatten.each do |cur_opts| cur_opts = prepare_options_hash(cur_opts) remove_non_filter_options(cur_opts) inner_result = filter_nodes(cur_opts, ref_node) result.nodes &= inner_result.nodes result.merge_attrs!(inner_result) end end
# File lib/webgen/node_finder.rb, line 319 def filter_descendants(result, ref_node, enabled) return unless enabled result.ref_node_used = true result.nodes.keep_if do |n| n.alcn.start_with?(ref_node.alcn) end end
# File lib/webgen/node_finder.rb, line 289 def filter_lang(result, ref_node, langs) langs = [langs].flatten.map do |l| if l == 'node' result.lang_used = true ref_node.lang else l end end.uniq fallback = langs.delete('fallback') result.nodes.keep_if do |n| langs.any? {|l| n.lang == l} || (fallback && n.lang == @website.config['website.lang'] && !n.tree.translations(n).any? {|tn| langs.any? {|l| tn.lang == l}}) end end
# File lib/webgen/node_finder.rb, line 265 def filter_meta_info(result, ref_node, mi) result.nodes.keep_if {|n| mi.all? {|key, val| n[key] == val}} end
# File lib/webgen/node_finder.rb, line 255 def filter_not(result, ref_node, opts) [opts].flatten.each do |cur_opts| cur_opts = prepare_options_hash(cur_opts) remove_non_filter_options(cur_opts) inner_result = filter_nodes(cur_opts, ref_node) result.nodes -= inner_result.nodes result.merge_attrs!(inner_result) end end
# File lib/webgen/node_finder.rb, line 245 def filter_or(result, ref_node, opts) [opts].flatten.each do |cur_opts| cur_opts = prepare_options_hash(cur_opts) remove_non_filter_options(cur_opts) inner_result = filter_nodes(cur_opts, ref_node) result.nodes |= inner_result.nodes result.merge_attrs!(inner_result) end end
# File lib/webgen/node_finder.rb, line 328 def filter_siblings(result, ref_node, value) return unless value result.parent_node_used = true if value == true result.nodes.keep_if {|n| n.parent == ref_node.parent} else lower, upper = *[value].flatten.map {|i| (i = i.to_i) < 0 ? ref_node.level + 1 + i : i} result.nodes.keep_if do |n| n.level >= lower && n.level <= upper && (n.parent.is_ancestor_of?(ref_node) || n.is_root?) end end end