# -*- encoding: utf-8 -*- require 'webgen/websiteaccess' require 'webgen/loggable' require 'webgen/path' require 'uri' require 'set' require 'pathname' module Webgen # Represents a file, a directory or a fragment. A node always belongs to a Tree. # # All needed meta and processing information is associated with a Node. The meta information is # available throught the #[] and #meta_info accessors, the processing information through the # #node_info accessor. # # Although node information should be changed by code, it is not advised to change meta # information values in code since this may lead to unwanted behaviour! class Node include WebsiteAccess include Loggable # The parent node. This is in all but one case a Node object. The one exception is that the # parent of the Tree#dummy_node is a Tree object. attr_reader :parent # The child nodes of this node. attr_reader :children # The full output path of this node. attr_reader :path # The tree to which this node belongs. attr_reader :tree # The canonical name of this node. attr_reader :cn # The absolute canonical name of this node. attr_reader :acn # The localized canonical name of this node. attr_reader :lcn # The absolute localized canonical name of this node. attr_reader :alcn # The level of the node. The level specifies how deep the node is in the hierarchy. attr_reader :level # The language of this node. attr_reader :lang # Meta information associated with the node. attr_reader :meta_info # Create a new Node instance. # # [+parent+ (immutable)] # The parent node under which this nodes should be created. # [+path+ (immutable)] # The full output path for this node. If this node is a directory, the path must have a # trailing slash (dir/). If it is a fragment, the hash sign must be the first # character of the path (#fragment). This can also be an absolute path like # http://myhost.com/. # [+cn+ (immutable)] # The canonical name for this node. Needs to be of the form basename.ext or # basename where +basename+ does not contain any dots. Also, the +basename+ must not # include a language part! # [+meta_info+] # A hash with meta information for the new node. # # The language of a node is taken from the meta information +lang+ and the entry is deleted from # the meta information hash. The language cannot be changed afterwards! If no +lang+ key is # found, the node is language neutral. def initialize(parent, path, cn, meta_info = {}) @parent = parent @cn = cn.freeze @children = [] reinit(path, meta_info) init_rest end # Re-initializes an already initialized node and resets it to its pristine state. def reinit(path, meta_info = {}) old_path = @path if defined?(@path) @path = path.freeze @lang = Webgen::LanguageManager.language_for_code(meta_info.delete('lang')) @lang = nil unless is_file? @meta_info = meta_info @flags = Set.new([:dirty, :created]) if defined?(@tree) @tree.node_access[:path].delete(old_path) if old_path @tree.register_path(self) self.node_info.clear self.node_info[:used_nodes] = Set.new self.node_info[:used_meta_info_nodes] = Set.new end end # Return the meta information item for +key+. def [](key) @meta_info[key] end # Assign +value+ to the meta information item for +key+. def []=(key, value) @meta_info[key] = value end # Return the node information hash which contains information for processing the node. def node_info tree.node_info[@alcn] ||= {} end # Check if the node is a directory. def is_directory?; @path[-1] == ?/ && !is_fragment?; end # Check if the node is a file. def is_file?; !is_directory? && !is_fragment?; end # Check if the node is a fragment. def is_fragment?; @cn[0] == ?# end # Check if the node is the root node. def is_root?; self == tree.root; end # Check if the node is flagged with one of the following: # # [:created] Has the node been created or has it been read from the cache? # [:reinit] Does the node need to be reinitialized? # [:dirty] Set by other objects to +true+ if they think the object has changed since the last # run. Must not be set to +false+ once it is +true+! # [:dirty_meta_info] Set by other objects to +true+ if the meta information of the node has # changed since the last run. Must not be set to +false+ once it is +true+! def flagged?(key) @flags.include?(key) end # Flag the node with the +keys+ and dispatch the message :node_flagged with +self+ and # +keys+ as arguments. See #flagged for valid keys. def flag(*keys) @flags += keys website.blackboard.dispatch_msg(:node_flagged, self, keys) end # Remove the flags +keys+ from the node and dispatch the message :node_unflagged with # +self+ and +keys+ as arguments. def unflag(*keys) @flags.subtract(keys) website.blackboard.dispatch_msg(:node_unflagged, self, keys) end # Return +true+ if the node has changed since the last webgen run. If it has changed, +dirty+ is # set to +true+. # # Sends the message :node_changed? with +self+ as argument unless the node is already # dirty. A listener to this message should set the flag :dirty on the passed node if he # thinks it is dirty. def changed? if_not_checked(:node) do flag(:dirty) if meta_info_changed? || user_nodes_changed? || node_info[:used_nodes].any? {|n| n != @alcn && (!tree[n] || tree[n].changed?)} || node_info[:used_meta_info_nodes].any? {|n| n != @alcn && (!tree[n] || tree[n].meta_info_changed?)} website.blackboard.dispatch_msg(:node_changed?, self) unless flagged?(:dirty) end flagged?(:dirty) end # Return +true+ if any node matching a pattern from the meta information +used_nodes+ has changed. def user_nodes_changed? pattern = [@meta_info['used_nodes']].flatten.compact.collect {|pat| Webgen::Path.make_absolute(parent.alcn, pat)} tree.node_access[:alcn].any? do |path, n| pattern.any? {|pat| n =~ pat && n.changed?} end if pattern.length > 0 end private :user_nodes_changed? # Return +true+ if the meta information of the node has changed. # # Sends the message :node_meta_info_changed? with +self+ as argument unless the meta # information of the node is already dirty. A listener to this message should set the flag # :dirt_meta_info on the passed node if he thinks that the node's meta information is # dirty. def meta_info_changed? if_not_checked(:meta_info) do website.blackboard.dispatch_msg(:node_meta_info_changed?, self) unless flagged?(:dirty_meta_info) end flagged?(:dirty_meta_info) end # Return an informative representation of the node. def inspect "<##{self.class.name}: alcn=#{@alcn}>" end # Return +true+ if the alcn matches the pattern. See File.fnmatch for useable patterns. def =~(pattern) File.fnmatch(pattern, @alcn, File::FNM_DOTMATCH|File::FNM_CASEFOLD|File::FNM_PATHNAME) end # Sort nodes by using the meta info +sort_info+ (or +title+ if +sort_info+ is not set) of both # involved nodes. def <=>(other) self_so = (@meta_info['sort_info'] && @meta_info['sort_info'].to_s) || @meta_info['title'] || '' other_so = (other['sort_info'] && other['sort_info'].to_s) || other['title'] || '' if self_so !~ /\D/ && other_so !~ /\D/ self_so = self_so.to_i other_so = other_so.to_i end self_so <=> other_so end # This pattern is the the same as URI::UNSAFE except that the hash character (#) is also # not escaped. This is needed sothat paths with fragments work correctly. URL_UNSAFE_PATTERN = Regexp.new("[^#{URI::PATTERN::UNRESERVED}#{URI::PATTERN::RESERVED}#]") # :nodoc: # Construct an internal URL for the given +name+ which can be an acn/alcn/path. If the parameter # +make_absolute+ is +true+, then a relative URL will be made absolute by prepending the special # URL webgen:://webgen.localhost/. def self.url(name, make_absolute = true) url = URI::parse(URI::escape(name, URL_UNSAFE_PATTERN)) url = URI::parse('webgen://webgen.localhost/') + url unless url.absolute? || !make_absolute url end # Check if the this node is in the subtree which is spanned by +node+. The check is performed # using only the +parent+ information of the involved nodes, NOT the actual path/alcn values! def in_subtree_of?(node) temp = self temp = temp.parent while temp != tree.dummy_root && temp != node temp != tree.dummy_root end # Return the node with the same canonical name but in language +lang+ or, if no such node # exists, an unlocalized version of the node. If no such node is found either, +nil+ is # returned. def in_lang(lang) avail = @tree.node_access[:acn][@acn] avail.find do |n| n = n.parent while n.is_fragment? n.lang == lang end || avail.find do |n| n = n.parent while n.is_fragment? n.lang.nil? end end # Return the node representing the given +path+ which can be an acn/alcn. The path can be # absolute (i.e. starting with a slash) or relative to the current node. If no node exists for # the given path or if the path is invalid, +nil+ is returned. # # If the +path+ is an alcn and a node is found, it is returned. If the +path+ is an acn, the # correct localized node according to +lang+ is returned or if no such node exists but an # unlocalized version does, the unlocalized node is returned. def resolve(path, lang = nil, use_passive_sources = true) orig_path = path url = self.class.url(@alcn) + self.class.url(path, false) path = url.path + (url.fragment.nil? ? '' : '#' + url.fragment) return nil if path =~ /^\/\.\./ node = @tree[path, :alcn] if !node || node.acn == path (node = (@tree[path, :acn] || @tree[path + '/', :acn])) && (node = node.in_lang(lang)) end if !node && use_passive_sources && !website.config['passive_sources'].empty? nodes = website.blackboard.invoke(:create_nodes_from_paths, [path]) node = resolve(orig_path, lang, false) node.node_info[:used_meta_info_nodes] += nodes.collect {|n| n.alcn} if node end node end # Return the relative path to the given path +other+. The parameter +other+ can be a Node or a # String. def route_to(other) my_url = self.class.url(@path) other_url = if other.kind_of?(Node) self.class.url(other.routing_node(@lang).path) elsif other.kind_of?(String) my_url + other else raise ArgumentError, "improper class for argument" end # resolve any '.' and '..' paths in the target url if other_url.path =~ /\/\.\.?\// && other_url.scheme == 'webgen' other_url.path = Pathname.new(other_url.path).cleanpath.to_s end route = my_url.route_to(other_url).to_s (route == '' ? File.basename(self.path) : route) end # Return the routing node in language +lang+ which is the node that is used when routing to this # node. The returned node can differ from the node itself in case of a directory where the # routing node is the directory index node. If +show_warning+ is +true+ and this node is a # directory node, then a warning is logged if no associated index file is found. def routing_node(lang, log_warning = true) if !is_directory? self else key = [alcn, :index_node, lang] vcache = website.cache.volatile return vcache[key] if vcache.has_key?(key) index_path = self.meta_info['index_path'] if index_path.nil? vcache[key] = self else index_node = resolve(index_path, lang) if index_node vcache[key] = index_node log(:info) { "Directory index path for <#{alcn}> => <#{index_node.alcn}>" } elsif log_warning vcache[key] = self log(:warn) { "No directory index path found for directory <#{alcn}>" } end end vcache[key] || self end end # Return a HTML link from this node to the +node+ or, if this node and +node+ are the same and # the parameter website.link_to_current_page is +false+, a +span+ element with the link # text. # # You can optionally specify additional attributes for the HTML element in the +attr+ Hash. # Also, the meta information +link_attrs+ of the given +node+ is used, if available, to set # attributes. However, the +attr+ parameter takes precedence over the +link_attrs+ meta # information. Be aware that all key-value pairs with Symbol keys are removed before the # attributes are written. Therefore you always need to specify general attributes with Strings! # # If the special value :link_text is present in the attributes, it will be used as the # link text; otherwise the title of the +node+ will be used. # # If the special value :lang is present in the attributes, it will be used as parameter # to the node.routing_node call for getting the linked-to node instead of this node's # +lang+ attribute. *Note*: this is only useful when linking to a directory. def link_to(node, attr = {}) attr = node['link_attrs'].merge(attr) if node['link_attrs'].kind_of?(Hash) rnode = node.routing_node(attr[:lang] || @lang) link_text = attr[:link_text] || (rnode != node && rnode['routed_title']) || node['title'] attr.delete_if {|k,v| k.kind_of?(Symbol)} use_link = (rnode != self || website.config['website.link_to_current_page']) attr['href'] = self.route_to(rnode) if use_link attrs = attr.collect {|name,value| "#{name.to_s}=\"#{value}\"" }.sort.unshift('').join(' ') (use_link ? "#{link_text}" : "#{link_text}") end ####### private ####### # Do the rest of the initialization. def init_rest @lcn = Path.lcn(@cn, @lang) @acn = (@parent.kind_of?(Tree) ? '' : @parent.acn.sub(/#.*$/, '') + @cn) @alcn = (@parent.kind_of?(Tree) ? '' : @parent.alcn.sub(/#.*$/, '') + @lcn) @level = -1 @tree = @parent (@level += 1; @tree = @tree.parent) while !@tree.kind_of?(Tree) @tree.register_node(self) @parent.children << self unless @parent == @tree self.node_info[:used_nodes] = Set.new self.node_info[:used_meta_info_nodes] = Set.new end # Only run the code in the block if this node has not already been checked. Different checks are # supported by setting a different +type+ value. def if_not_checked(type) array = (website.cache.volatile[:node_change_checking] ||= {})[type] ||= [] if !array.include?(self) array << self yield array.delete(self) end end # Delegate missing methods to a processor. The current node is placed into the argument array as # the first argument before the method +name+ is invoked on the processor. def method_missing(name, *args, &block) if node_info[:processor] website.cache.instance(node_info[:processor]).send(name, *([self] + args), &block) else super end end end end