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:

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_used must not be set to false once it is true!

Public Class Methods

new(website) click to toggle source

Create a new NodeFinder object for the given website.

   # File lib/webgen/node_finder.rb
75 def initialize(website)
76   @website = website
77   @mapping = {
78     :alcn => :filter_alcn, :absolute_levels => :filter_absolute_levels, :lang => :filter_lang,
79     :and => :filter_and, :or => :filter_or, :not => :filter_not,
80     :ancestors => :filter_ancestors, :descendants => :filter_descendants,
81     :siblings => :filter_siblings,
82     :mi => :filter_meta_info
83   }
84 end

Public Instance Methods

add_filter_module(mod, mapping) click to toggle source

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
 95 def add_filter_module(mod, mapping)
 96   public_methods = mod.public_instance_methods.map {|c| c.to_s}
 97   mapping.each do |name, method|
 98     if !public_methods.include?(method.to_s)
 99       raise ArgumentError, "Finder method '#{method}' not found in module #{mod}"
100     end
101     @mapping[name.intern] = method
102   end
103   extend(mod)
104 end
find(opts_or_name, ref_node) click to toggle source

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
111 def find(opts_or_name, ref_node)
112   if result = cached_result(opts_or_name, ref_node)
113     return result.nodes
114   end
115   opts = prepare_options_hash(opts_or_name)
116 
117   limit, offset, flatten, sort, levels, reverse = remove_non_filter_options(opts)
118   flatten = true if limit || offset
119   levels = [levels || [1, 1_000_000]].flatten.map {|i| i.to_i}
120 
121   result = filter_nodes(opts, ref_node)
122   nodes = result.nodes
123 
124   if flatten
125     sort_nodes(nodes, sort, reverse)
126     nodes = nodes[(offset.to_s.to_i)..(limit ? offset.to_s.to_i + limit.to_s.to_i - 1 : -1)] if limit || offset
127   else
128     temp = {}
129     min_level = 1_000_000
130     nodes.each {|n| min_level = n.level if n.level < min_level}
131 
132     nodes.each do |n|
133       hierarchy_nodes = []
134       (hierarchy_nodes.unshift(n); n = n.parent) while n.level >= min_level
135       hierarchy_nodes.inject(temp) {|memo, hn| memo[hn] ||= {}}
136     end
137 
138     reducer = lambda do |h, level|
139       if level < levels.first
140         temp = h.map {|k,v| v.empty? ? nil : reducer.call(v, level + 1)}.compact
141         temp.length == 1 && temp.first.kind_of?(Array) ? temp.first : temp
142       elsif level < levels.last
143         h.map {|k,v| v.empty? ? k : [k, reducer.call(v, level + 1)]}
144       else
145         h.map {|k,v| k}
146       end
147     end
148     nodes = reducer.call(temp, 1)
149     sort_nodes(nodes, sort, reverse, false)
150   end
151 
152   result.nodes = nodes
153   cache_result(opts_or_name, ref_node, result)
154   result.nodes
155 end

Private Instance Methods

cache_key(opts, ref_node, result) click to toggle source
    # File lib/webgen/node_finder.rb
170 def cache_key(opts, ref_node, result)
171   [opts,
172    result.ref_node_used && ref_node.alcn,
173    result.lang_used && ref_node.lang,
174    result.level_used && ref_node.level,
175    result.parent_node_used && ref_node.parent.alcn]
176 end
cache_result(opts, ref_node, result) click to toggle source
    # File lib/webgen/node_finder.rb
165 def cache_result(opts, ref_node, result)
166   result_cache[opts] = result
167   result_cache[cache_key(opts, ref_node, result)] = result
168 end
cached_result(opts, ref_node) click to toggle source
    # File lib/webgen/node_finder.rb
161 def cached_result(opts, ref_node)
162   (result = result_cache[opts]) && result_cache[cache_key(opts, ref_node, result)]
163 end
filter_nodes(opts, ref_node) click to toggle source
    # File lib/webgen/node_finder.rb
197 def filter_nodes(opts, ref_node)
198   nodes = @website.tree.node_access[:alcn].values
199   nodes.delete(@website.tree.dummy_root)
200 
201   result = Result.new(nodes, false)
202 
203   opts.each do |filter, value|
204     if @mapping.has_key?(filter)
205       send(@mapping[filter], result, ref_node, value)
206     else
207       @website.logger.warn { "Ignoring unknown node finder filter '#{filter}'" }
208     end
209   end
210 
211   result
212 end
prepare_options_hash(opts_or_name) click to toggle source
    # File lib/webgen/node_finder.rb
182 def prepare_options_hash(opts_or_name)
183   if Hash === opts_or_name
184     opts_or_name.symbolize_keys
185   elsif @website.config['node_finder.option_sets'].has_key?(opts_or_name)
186     @website.config['node_finder.option_sets'][opts_or_name].symbolize_keys
187   else
188     raise ArgumentError, "Invalid argument supplied, expected Hash or name of search definition, not #{opts_or_name}"
189   end
190 end
remove_non_filter_options(opts) click to toggle source
    # File lib/webgen/node_finder.rb
192 def remove_non_filter_options(opts)
193   [opts.delete(:limit), opts.delete(:offset), opts.delete(:flatten),
194    opts.delete(:sort), opts.delete(:levels), opts.delete(:reverse)]
195 end
result_cache() click to toggle source
    # File lib/webgen/node_finder.rb
178 def result_cache
179   @website.cache.volatile[:node_finder] ||= {}
180 end
sort_nodes(nodes, sort, reverse, flat_mode = true) click to toggle source
    # File lib/webgen/node_finder.rb
214 def sort_nodes(nodes, sort, reverse, flat_mode = true)
215   return unless sort
216   if sort == true
217     nodes.sort! do |(a,_),(b,_)|
218       a = (a['sort_info'] && a['sort_info'].to_s) || a['title'].to_s || ''
219       b = (b['sort_info'] && b['sort_info'].to_s) || b['title'].to_s || ''
220       (a = a.to_i; b = b.to_i) if a !~ /\D/ && b !~ /\D/
221       (reverse ? b <=> a : a <=> b)
222     end
223   else
224     nodes.sort! do |(a,_),(b,_)|
225       a, b = a[sort].to_s, b[sort].to_s
226       a, b = a.to_i, b.to_i if a !~ /\D/ && b !~ /\D/
227       (reverse ? b <=> a : a <=> b)
228     end
229   end
230   nodes.each {|n, children| sort_nodes(children, sort, reverse, flat_mode) if children } unless flat_mode
231 end

Filter methods

↑ top

Private Instance Methods

filter_absolute_levels(result, ref_node, range) click to toggle source
    # File lib/webgen/node_finder.rb
277 def filter_absolute_levels(result, ref_node, range)
278   range = [range].flatten.map do |i|
279     if (i = i.to_i) < 0
280       result.level_used = true
281       ref_node.level + 1 + i
282     else
283       i
284     end
285   end
286   result.nodes.keep_if {|n| n.level >= range.first && n.level <= range.last}
287 end
filter_alcn(result, ref_node, alcn) click to toggle source
    # File lib/webgen/node_finder.rb
269 def filter_alcn(result, ref_node, alcn)
270   alcn = [alcn].flatten.map do |a|
271     result.ref_node_used = true unless a.to_s.start_with?('/')
272     Webgen::Path.append(ref_node.alcn, a.to_s)
273   end
274   result.nodes.keep_if {|n| alcn.any? {|a| n =~ a}}
275 end
filter_ancestors(result, ref_node, enabled) click to toggle source
    # File lib/webgen/node_finder.rb
306 def filter_ancestors(result, ref_node, enabled)
307   return unless enabled
308   result.ref_node_used = true
309 
310   nodes = []
311   node = ref_node
312   until node == node.tree.dummy_root
313     nodes.unshift(node)
314     node = node.parent
315   end
316   result.nodes = nodes & result.nodes
317 end
filter_and(result, ref_node, opts) click to toggle source
    # File lib/webgen/node_finder.rb
235 def filter_and(result, ref_node, opts)
236   [opts].flatten.each do |cur_opts|
237     cur_opts = prepare_options_hash(cur_opts)
238     remove_non_filter_options(cur_opts)
239     inner_result = filter_nodes(cur_opts, ref_node)
240     result.nodes &= inner_result.nodes
241     result.merge_attrs!(inner_result)
242   end
243 end
filter_descendants(result, ref_node, enabled) click to toggle source
    # File lib/webgen/node_finder.rb
319 def filter_descendants(result, ref_node, enabled)
320   return unless enabled
321   result.ref_node_used = true
322 
323   result.nodes.keep_if do |n|
324     n.alcn.start_with?(ref_node.alcn)
325   end
326 end
filter_lang(result, ref_node, langs) click to toggle source
    # File lib/webgen/node_finder.rb
289 def filter_lang(result, ref_node, langs)
290   langs = [langs].flatten.map do |l|
291     if l == 'node'
292       result.lang_used = true
293       ref_node.lang
294     else
295       l
296     end
297   end.uniq
298   fallback = langs.delete('fallback')
299   result.nodes.keep_if do |n|
300     langs.any? {|l| n.lang == l} ||
301       (fallback && n.lang == @website.config['website.lang'] &&
302        !n.tree.translations(n).any? {|tn| langs.any? {|l| tn.lang == l}})
303   end
304 end
filter_meta_info(result, ref_node, mi) click to toggle source
    # File lib/webgen/node_finder.rb
265 def filter_meta_info(result, ref_node, mi)
266   result.nodes.keep_if {|n| mi.all? {|key, val| n[key] == val}}
267 end
filter_not(result, ref_node, opts) click to toggle source
    # File lib/webgen/node_finder.rb
255 def filter_not(result, ref_node, opts)
256   [opts].flatten.each do |cur_opts|
257     cur_opts = prepare_options_hash(cur_opts)
258     remove_non_filter_options(cur_opts)
259     inner_result = filter_nodes(cur_opts, ref_node)
260     result.nodes -= inner_result.nodes
261     result.merge_attrs!(inner_result)
262   end
263 end
filter_or(result, ref_node, opts) click to toggle source
    # File lib/webgen/node_finder.rb
245 def filter_or(result, ref_node, opts)
246   [opts].flatten.each do |cur_opts|
247     cur_opts = prepare_options_hash(cur_opts)
248     remove_non_filter_options(cur_opts)
249     inner_result = filter_nodes(cur_opts, ref_node)
250     result.nodes |= inner_result.nodes
251     result.merge_attrs!(inner_result)
252   end
253 end
filter_siblings(result, ref_node, value) click to toggle source
    # File lib/webgen/node_finder.rb
328 def filter_siblings(result, ref_node, value)
329   return unless value
330   result.parent_node_used = true
331 
332   if value == true
333     result.nodes.keep_if {|n| n.parent == ref_node.parent}
334   else
335     lower, upper = *[value].flatten.map {|i| (i = i.to_i) < 0 ? ref_node.level + 1 + i : i}
336     result.nodes.keep_if do |n|
337       n.level >= lower && n.level <= upper && (n.parent.is_ancestor_of?(ref_node) || n.is_root?)
338     end
339   end
340 end