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_used
has to be set totrue
. -
If the node's hierarchy level is used,
result.level_used
has to be set totrue
. -
If the node's parent is used,
result.parent_node_used
has to be set totrue
. -
In all other cases,
result.ref_node_used
has 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_used
must not be set tofalse
once it istrue
!
Public Class Methods
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 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
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
# 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
# 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
# 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
# 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
# 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
# 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
# File lib/webgen/node_finder.rb 178 def result_cache 179 @website.cache.volatile[:node_finder] ||= {} 180 end
# 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
↑ topPrivate Instance Methods
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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